gestalt-mobile 0.41.1 → 0.41.3
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-G2PznC5F.js → index-BRly-q56.js} +23 -23
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +192 -50
- package/dist/server/server/composition.js +175 -33
- package/dist/server/server/features/agent-activity/model.js +8 -2
- package/dist/server/server/features/autopilot/application/policy.js +4 -4
- package/dist/server/server/features/autopilot/application/service.js +103 -15
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +9 -4
- package/dist/server/server/features/org-plan-attention/resolve/endpoint.js +5 -3
- package/dist/server/server/features/org-plan-checkpoint/application/validate.js +83 -15
- package/dist/server/server/features/plans/application/supervised-plan-registry.js +8 -1
- package/dist/server/server/features/skills/list-available/endpoint.js +2 -1
- package/dist/server/server/features/skills/model/skill-profile.js +8 -4
- package/dist/server/server/platform/catalog/profile-command.js +2 -12
- package/dist/server/server/platform/codex/codex-process-launcher.js +1 -1
- package/dist/server/server/platform/codex/session-runtime.js +16 -8
- package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +12 -6
- package/dist/server/shared/contracts/org-plan-attention.js +43 -7
- package/dist/server/shared/contracts/org-plan-checkpoint.js +5 -1
- package/dist/server/shared/contracts/plan-signal.js +3 -1
- package/package.json +1 -1
|
@@ -321,9 +321,9 @@ export class AutopilotCoordinator {
|
|
|
321
321
|
consecutiveNoProgress: 0,
|
|
322
322
|
nextEvaluationAt: null,
|
|
323
323
|
stopReason: null,
|
|
324
|
-
executor: undefined,
|
|
324
|
+
executor: !replacing && prior.state === 'attentionRequired' ? prior.executor : undefined,
|
|
325
325
|
blocking: undefined,
|
|
326
|
-
supervision: recoverSafetyPause(consumeObsoleteWait(replacing
|
|
326
|
+
supervision: recoverSafetyPause(consumeObsoleteWait(replacing || prior.supervision?.outcome === 'attentionRequired'
|
|
327
327
|
? startSupervisionProtocol(this.progressKey(sessionId))
|
|
328
328
|
: (prior.supervision ?? startSupervisionProtocol(this.progressKey(sessionId))), this.progressKey(sessionId)), this.progressKey(sessionId)),
|
|
329
329
|
updatedAt: now,
|
|
@@ -332,6 +332,64 @@ export class AutopilotCoordinator {
|
|
|
332
332
|
this.evaluate(sessionId);
|
|
333
333
|
return this.snapshot(sessionId);
|
|
334
334
|
}
|
|
335
|
+
/** Converts one typed human permission into one exact durable physical replacement. */
|
|
336
|
+
authorizeExecutorReplacement(sessionId, canonicalTaskName) {
|
|
337
|
+
const state = this.deps.store.find(sessionId);
|
|
338
|
+
const retained = this.deps.plan(sessionId);
|
|
339
|
+
if (!state ||
|
|
340
|
+
state.state !== 'attentionRequired' ||
|
|
341
|
+
state.blocking?.reason !== 'permissionRequired')
|
|
342
|
+
return { accepted: false, reason: 'attentionRequired' };
|
|
343
|
+
if (!retained ||
|
|
344
|
+
state.planIdentity !== retained.identity ||
|
|
345
|
+
state.planFingerprint !== fingerprint(retained.plan))
|
|
346
|
+
return { accepted: false, reason: 'planMismatch' };
|
|
347
|
+
const executor = state.executor;
|
|
348
|
+
if (!executor || executor.canonicalTaskName !== canonicalTaskName || executor.l1State !== 'WIP')
|
|
349
|
+
return { accepted: false, reason: 'executorNotAssigned' };
|
|
350
|
+
if (executor.replacement && this.validReplacement(sessionId, executor.replacement))
|
|
351
|
+
return { accepted: true, identity: executor.replacement };
|
|
352
|
+
const activity = this.deps.activity(sessionId);
|
|
353
|
+
if (activity?.confidence !== 'fresh' || this.executorTurnActive(executor, activity))
|
|
354
|
+
return { accepted: false, reason: 'executorStillActive' };
|
|
355
|
+
const previous = executor.assignment ?? executorAssignment(executor);
|
|
356
|
+
const identity = executorIdentity(canonicalTaskName, previous.generation + 1);
|
|
357
|
+
const now = this.deps.now();
|
|
358
|
+
this.persist({
|
|
359
|
+
...state,
|
|
360
|
+
executor: {
|
|
361
|
+
...executor,
|
|
362
|
+
outcome: 'failed',
|
|
363
|
+
assignment: { ...previous, state: 'unavailable' },
|
|
364
|
+
replacement: {
|
|
365
|
+
...identity,
|
|
366
|
+
planIdentity: retained.identity,
|
|
367
|
+
planFingerprint: fingerprint(retained.plan),
|
|
368
|
+
previous: { ...previous, state: 'unavailable' },
|
|
369
|
+
reason: 'humanPermissionGranted',
|
|
370
|
+
evidence: 'typed-attention-approval',
|
|
371
|
+
},
|
|
372
|
+
commands: executor.commands?.map((command) => ['scheduled', 'issued'].includes(command.status)
|
|
373
|
+
? { ...command, status: 'superseded', updatedAt: now }
|
|
374
|
+
: command),
|
|
375
|
+
},
|
|
376
|
+
updatedAt: now,
|
|
377
|
+
}, undefined, [
|
|
378
|
+
{
|
|
379
|
+
sessionId,
|
|
380
|
+
type: 'autopilot.executor-replacement-authorized',
|
|
381
|
+
payload: {
|
|
382
|
+
canonicalTaskName,
|
|
383
|
+
previousTaskName: previous.taskName,
|
|
384
|
+
taskName: identity.taskName,
|
|
385
|
+
generation: identity.generation,
|
|
386
|
+
evidence: 'typed-attention-approval',
|
|
387
|
+
},
|
|
388
|
+
occurredAt: now,
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
return { accepted: true, identity };
|
|
392
|
+
}
|
|
335
393
|
disable(sessionId) {
|
|
336
394
|
const now = this.deps.now();
|
|
337
395
|
const prior = this.deps.store.find(sessionId) ?? disabledAutopilot(sessionId, now);
|
|
@@ -421,6 +479,7 @@ export class AutopilotCoordinator {
|
|
|
421
479
|
if (this.deps.executorController &&
|
|
422
480
|
plan &&
|
|
423
481
|
!this.requiresRootBoundary(prior, plan) &&
|
|
482
|
+
!this.executorDiagnosisRequired(prior) &&
|
|
424
483
|
this.currentExecutor(sessionId, plan, prior.consecutiveNoProgress, prior.executor)) {
|
|
425
484
|
this.enqueue(sessionId, async () => {
|
|
426
485
|
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
@@ -885,15 +944,27 @@ export class AutopilotCoordinator {
|
|
|
885
944
|
const cancelled = this.cancelScheduledControl(prior, now);
|
|
886
945
|
const subagentsWorking = activity.aggregateSubagents === 'working' ||
|
|
887
946
|
activity.aggregateSubagents === 'awaitingAgent';
|
|
947
|
+
const supervision = subagentsWorking &&
|
|
948
|
+
Boolean(this.deps.session(sessionId)?.activeTurnId) &&
|
|
949
|
+
prior.supervision?.outcome === 'probeRequired'
|
|
950
|
+
? reportProbe(prior.supervision, {
|
|
951
|
+
id: `executor-actionable:${createHash('sha256')
|
|
952
|
+
.update(this.progressKey(sessionId))
|
|
953
|
+
.digest('hex')}`,
|
|
954
|
+
kind: 'actionable',
|
|
955
|
+
})
|
|
956
|
+
: prior.supervision;
|
|
888
957
|
if (cancelled ||
|
|
889
958
|
prior.state !== 'monitoring' ||
|
|
890
959
|
prior.nextEvaluationAt ||
|
|
891
|
-
(subagentsWorking && prior.consecutiveNoProgress > 0)
|
|
960
|
+
(subagentsWorking && prior.consecutiveNoProgress > 0) ||
|
|
961
|
+
supervision !== prior.supervision)
|
|
892
962
|
this.persist({
|
|
893
963
|
...prior,
|
|
894
964
|
state: 'monitoring',
|
|
895
965
|
...(cancelled ? { generation: prior.generation + 1, lastControlId: null } : {}),
|
|
896
966
|
...(subagentsWorking ? { consecutiveNoProgress: 0 } : {}),
|
|
967
|
+
...(supervision ? { supervision } : {}),
|
|
897
968
|
nextEvaluationAt: null,
|
|
898
969
|
updatedAt: now,
|
|
899
970
|
}, cancelled);
|
|
@@ -1033,6 +1104,7 @@ export class AutopilotCoordinator {
|
|
|
1033
1104
|
retained &&
|
|
1034
1105
|
!current.executor?.replacement &&
|
|
1035
1106
|
!this.requiresRootBoundary(current, retained.plan) &&
|
|
1107
|
+
!this.executorDiagnosisRequired(current) &&
|
|
1036
1108
|
this.currentExecutor(sessionId, retained.plan, current.consecutiveNoProgress, current.executor)) {
|
|
1037
1109
|
const now = this.deps.now();
|
|
1038
1110
|
const cancelled = this.cancelScheduledControl(current, now);
|
|
@@ -1047,7 +1119,12 @@ export class AutopilotCoordinator {
|
|
|
1047
1119
|
await this.enforceSupervisedLifecycle(sessionId, 'stateChanged');
|
|
1048
1120
|
return;
|
|
1049
1121
|
}
|
|
1050
|
-
|
|
1122
|
+
// Direct executor continuations account for their own accepted attempts.
|
|
1123
|
+
// Once that bounded loop requests diagnosis, preserve the probe state for
|
|
1124
|
+
// the root instead of counting the root control as another blind retry.
|
|
1125
|
+
const protocol = this.executorDiagnosisRequired(current)
|
|
1126
|
+
? current.supervision
|
|
1127
|
+
: recordAutomaticContinuation(current.supervision ?? startSupervisionProtocol(this.progressKey(sessionId)), this.progressKey(sessionId));
|
|
1051
1128
|
if (protocol.outcome === 'probeRequired') {
|
|
1052
1129
|
this.persist({ ...current, supervision: protocol, updatedAt: this.deps.now() }, undefined, [
|
|
1053
1130
|
{
|
|
@@ -1224,6 +1301,8 @@ export class AutopilotCoordinator {
|
|
|
1224
1301
|
case 'reinspect':
|
|
1225
1302
|
return false;
|
|
1226
1303
|
case 'resumeExecutor':
|
|
1304
|
+
if (this.executorDiagnosisRequired(this.deps.store.find(sessionId) ?? state))
|
|
1305
|
+
return false;
|
|
1227
1306
|
this.armExecutorContinuation(sessionId, decision.action.delayMs, decision.action.threadId, decision.action.generation, { kind: 'partial' });
|
|
1228
1307
|
return true;
|
|
1229
1308
|
case 'monitorProcess': {
|
|
@@ -1258,7 +1337,7 @@ export class AutopilotCoordinator {
|
|
|
1258
1337
|
resultArtifact: processAction.resultArtifact,
|
|
1259
1338
|
});
|
|
1260
1339
|
if (consumed && executor)
|
|
1261
|
-
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, {
|
|
1340
|
+
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + executor.continuationCount + 1, {
|
|
1262
1341
|
kind: 'processExited',
|
|
1263
1342
|
processId,
|
|
1264
1343
|
resultArtifact: processAction.resultArtifact,
|
|
@@ -1279,7 +1358,7 @@ export class AutopilotCoordinator {
|
|
|
1279
1358
|
reason: 'resourceBudget',
|
|
1280
1359
|
});
|
|
1281
1360
|
if (terminated && executor)
|
|
1282
|
-
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + 1, { kind: 'processResourceLimit', processId });
|
|
1361
|
+
this.armExecutorContinuation(sessionId, this.deps.policy.executorContinuationBaseMs, executor.threadId, executor.continuationGeneration + executor.continuationCount + 1, { kind: 'processResourceLimit', processId });
|
|
1283
1362
|
else
|
|
1284
1363
|
this.armExecutorRefresh(sessionId, this.deps.policy.processPollMs);
|
|
1285
1364
|
return true;
|
|
@@ -1366,7 +1445,9 @@ export class AutopilotCoordinator {
|
|
|
1366
1445
|
ownedProcesses: refreshPersistedProcesses(persisted?.canonicalPosition === canonicalPosition ? persisted.ownedProcesses : [], child.ownedProcesses ?? [], this.deps.now()),
|
|
1367
1446
|
outcome: outcome.outcome,
|
|
1368
1447
|
...(outcome.blocking ? { blocking: outcome.blocking } : {}),
|
|
1369
|
-
|
|
1448
|
+
// The roster value and assignment identify the physical l<a>_gN slot.
|
|
1449
|
+
// Logical follow-up attempts are tracked separately by continuationCount.
|
|
1450
|
+
continuationGeneration: replacementChild || !assigned ? (child.continuationGeneration ?? 1) : assigned.generation,
|
|
1370
1451
|
continuationCount: persisted?.canonicalPosition === canonicalPosition
|
|
1371
1452
|
? persisted.continuationCount
|
|
1372
1453
|
: continuationCount,
|
|
@@ -1892,23 +1973,24 @@ export class AutopilotCoordinator {
|
|
|
1892
1973
|
const armedChild = this.deps
|
|
1893
1974
|
.activity(sessionId)
|
|
1894
1975
|
?.subagents.find((child) => (child.threadId ?? child.id) === threadId);
|
|
1895
|
-
const
|
|
1976
|
+
const commandExecutor = state?.executor;
|
|
1977
|
+
const fence = state && retained && commandExecutor
|
|
1896
1978
|
? {
|
|
1897
1979
|
sessionGeneration: state.generation,
|
|
1898
1980
|
planIdentity: retained.identity,
|
|
1899
1981
|
planFingerprint: fingerprint(retained.plan),
|
|
1900
1982
|
canonicalPosition: armedChild?.canonicalPosition ?? state.executor?.canonicalPosition ?? null,
|
|
1901
1983
|
executorThreadId: threadId,
|
|
1902
|
-
|
|
1984
|
+
// The roster generation identifies the physical l<a>_gN slot.
|
|
1985
|
+
// A canonical executor can accept many logical follow-up turns, so
|
|
1986
|
+
// never derive this ownership fence from the follow-up sequence.
|
|
1987
|
+
executorGeneration: commandExecutor.assignment?.generation ?? commandExecutor.continuationGeneration,
|
|
1903
1988
|
supervisionOutcome: state.supervision?.outcome ?? null,
|
|
1904
1989
|
checkpoint: JSON.stringify(state.checkpoints ?? null),
|
|
1905
1990
|
boundaryState: state.state,
|
|
1906
1991
|
}
|
|
1907
1992
|
: null;
|
|
1908
|
-
if (!fence || this.deps.session(sessionId)?.activeTurnId)
|
|
1909
|
-
return;
|
|
1910
|
-
const commandExecutor = state?.executor;
|
|
1911
|
-
if (!commandExecutor)
|
|
1993
|
+
if (!commandExecutor || !fence || this.deps.session(sessionId)?.activeTurnId)
|
|
1912
1994
|
return;
|
|
1913
1995
|
const requested = this.executorCommand(fence, commandExecutor, generation, trigger);
|
|
1914
1996
|
const command = this.scheduleExecutorCommand(sessionId, requested);
|
|
@@ -1974,17 +2056,19 @@ export class AutopilotCoordinator {
|
|
|
1974
2056
|
trigger: trigger.kind,
|
|
1975
2057
|
});
|
|
1976
2058
|
const latest = this.deps.store.find(sessionId);
|
|
1977
|
-
if (latest?.requestedEnabled)
|
|
2059
|
+
if (latest?.requestedEnabled) {
|
|
2060
|
+
const progressKey = this.progressKey(sessionId);
|
|
2061
|
+
const supervision = recordAutomaticContinuation(latest.supervision ?? startSupervisionProtocol(progressKey), progressKey);
|
|
1978
2062
|
this.persist({
|
|
1979
2063
|
...latest,
|
|
1980
2064
|
state: 'monitoring',
|
|
2065
|
+
supervision,
|
|
1981
2066
|
consecutiveNoProgress: latest.consecutiveNoProgress + 1,
|
|
1982
2067
|
...(latest.executor
|
|
1983
2068
|
? {
|
|
1984
2069
|
executor: {
|
|
1985
2070
|
...latest.executor,
|
|
1986
2071
|
outcome: 'partial',
|
|
1987
|
-
continuationGeneration: generation,
|
|
1988
2072
|
continuationCount: latest.executor.continuationCount + 1,
|
|
1989
2073
|
lastActivityAt: this.deps.now(),
|
|
1990
2074
|
},
|
|
@@ -1993,6 +2077,7 @@ export class AutopilotCoordinator {
|
|
|
1993
2077
|
nextEvaluationAt: null,
|
|
1994
2078
|
updatedAt: this.deps.now(),
|
|
1995
2079
|
});
|
|
2080
|
+
}
|
|
1996
2081
|
}
|
|
1997
2082
|
catch (error) {
|
|
1998
2083
|
// Only an explicit app-server rejection is safe to retry. A lost
|
|
@@ -2085,6 +2170,9 @@ export class AutopilotCoordinator {
|
|
|
2085
2170
|
child.state === 'awaitingAgent' ||
|
|
2086
2171
|
child.ownedProcesses?.some((process) => process.state === 'running' || process.state === 'detached-active')));
|
|
2087
2172
|
}
|
|
2173
|
+
executorDiagnosisRequired(state) {
|
|
2174
|
+
return state.supervision?.outcome === 'probeRequired';
|
|
2175
|
+
}
|
|
2088
2176
|
executorTurnActive(executor, activity) {
|
|
2089
2177
|
if (activity?.confidence !== 'fresh')
|
|
2090
2178
|
return false;
|
|
@@ -179,7 +179,8 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
179
179
|
const replacementPrevious = replacementPreviousValue
|
|
180
180
|
? parseExecutorAssignment(replacementPreviousValue)
|
|
181
181
|
: undefined;
|
|
182
|
-
const replacementReason = replacementValue &&
|
|
182
|
+
const replacementReason = replacementValue &&
|
|
183
|
+
stringValue(replacementValue.reason, ['explicitExecutorRejection', 'humanPermissionGranted']);
|
|
183
184
|
const replacementEvidence = replacementValue && boundedText(replacementValue.evidence);
|
|
184
185
|
if (!canonicalPosition ||
|
|
185
186
|
!canonicalTaskName ||
|
|
@@ -200,7 +201,8 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
200
201
|
!replacementPlanIdentity ||
|
|
201
202
|
!replacementPlanFingerprint ||
|
|
202
203
|
(replacementValue.previous !== undefined && !replacementPrevious) ||
|
|
203
|
-
(replacementReason !== undefined &&
|
|
204
|
+
(replacementReason !== undefined &&
|
|
205
|
+
!['explicitExecutorRejection', 'humanPermissionGranted'].includes(replacementReason)) ||
|
|
204
206
|
(replacementValue.evidence !== undefined && !replacementEvidence))))
|
|
205
207
|
return undefined;
|
|
206
208
|
if (replacementValue) {
|
|
@@ -304,7 +306,10 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
304
306
|
ownedProcesses,
|
|
305
307
|
outcome,
|
|
306
308
|
...(executorBlocking ? { blocking: executorBlocking } : {}),
|
|
307
|
-
|
|
309
|
+
// Earlier releases incremented this value after every follow-up, even
|
|
310
|
+
// though the roster derives it from the immutable physical task name. A
|
|
311
|
+
// durable assignment is authoritative during reconciliation.
|
|
312
|
+
continuationGeneration: assignment?.generation ?? continuationGeneration,
|
|
308
313
|
continuationCount,
|
|
309
314
|
...(resumeFailures ? { resumeFailures } : {}),
|
|
310
315
|
...(assignment ? { assignment } : {}),
|
|
@@ -537,7 +542,7 @@ export function decideSupervisedLifecycle(input) {
|
|
|
537
542
|
action: {
|
|
538
543
|
kind: 'resumeExecutor',
|
|
539
544
|
threadId: input.executor.threadId,
|
|
540
|
-
generation: input.executor.continuationGeneration + 1,
|
|
545
|
+
generation: input.executor.continuationGeneration + input.executor.continuationCount + 1,
|
|
541
546
|
delayMs,
|
|
542
547
|
},
|
|
543
548
|
};
|
|
@@ -34,9 +34,11 @@ export function registerResolveOrgPlanAttention(app, deps) {
|
|
|
34
34
|
? 'ATTENTION_OPERATION_STALE'
|
|
35
35
|
: outcome.kind === 'legacyUnsupported'
|
|
36
36
|
? 'ATTENTION_LEGACY_UNSUPPORTED'
|
|
37
|
-
: outcome.kind === '
|
|
38
|
-
? '
|
|
39
|
-
: '
|
|
37
|
+
: outcome.kind === 'replacementRejected'
|
|
38
|
+
? 'EXECUTOR_REPLACEMENT_NOT_AUTHORIZED'
|
|
39
|
+
: outcome.kind === 'writerCleared'
|
|
40
|
+
? 'ATTENTION_WRITER_CLEARED'
|
|
41
|
+
: 'ATTENTION_WRITER_UNAVAILABLE';
|
|
40
42
|
return reply.code(outcome.kind === 'noActive' ? 404 : 409).send({ code });
|
|
41
43
|
});
|
|
42
44
|
}
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
+
/** Terminal roster entries remain visible and are not active implementation writers. */
|
|
7
|
+
export function hasActiveL1Writer(snapshot, position) {
|
|
8
|
+
return snapshot.subagents.some((child) => child.canonicalPosition === position &&
|
|
9
|
+
['working', 'awaitingAgent', 'awaitingHuman'].includes(child.state));
|
|
10
|
+
}
|
|
6
11
|
/** Expands a host-owned compact boundary signal from authoritative plan state. */
|
|
7
|
-
export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
|
|
12
|
+
export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity, publicationReason = null) {
|
|
8
13
|
if (kind === 'terminalReviewAccepted')
|
|
9
14
|
return {
|
|
10
15
|
version: 1,
|
|
@@ -12,8 +17,16 @@ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
|
|
|
12
17
|
planIdentity,
|
|
13
18
|
verdict: 'ACCEPT',
|
|
14
19
|
};
|
|
15
|
-
const
|
|
16
|
-
|
|
20
|
+
const reviewedId = publicationReason?.match(/^review:([^:]{1,128}):REVIEWED$/)?.[1];
|
|
21
|
+
const selected = kind === 'l1Accepted'
|
|
22
|
+
? ((reviewedId
|
|
23
|
+
? plan.steps.find((step) => step.id === reviewedId && step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
|
|
24
|
+
: undefined) ??
|
|
25
|
+
[...plan.steps]
|
|
26
|
+
.reverse()
|
|
27
|
+
.find((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED'))
|
|
28
|
+
: (plan.steps.find((step) => step.id === plan.currentStepId) ??
|
|
29
|
+
[...plan.steps].reverse().find((step) => step.state === 'DONE' || step.state === 'WIP'));
|
|
17
30
|
if (!selected)
|
|
18
31
|
return null;
|
|
19
32
|
const l1Position = `L${plan.steps.indexOf(selected) + 1}`;
|
|
@@ -44,25 +57,80 @@ export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
|
|
|
44
57
|
};
|
|
45
58
|
}
|
|
46
59
|
export function validOrgPlanCheckpoint(input) {
|
|
60
|
+
return validateOrgPlanCheckpoint(input).valid;
|
|
61
|
+
}
|
|
62
|
+
export function validateOrgPlanCheckpoint(input) {
|
|
47
63
|
const { checkpoint, plan, planIdentity, rootOwned } = input;
|
|
48
|
-
if (!rootOwned
|
|
49
|
-
return false;
|
|
64
|
+
if (!rootOwned)
|
|
65
|
+
return { valid: false, reasonCode: 'rootNotOwner' };
|
|
66
|
+
if (checkpoint.planIdentity !== planIdentity)
|
|
67
|
+
return { valid: false, reasonCode: 'planIdentityMismatch' };
|
|
50
68
|
if (checkpoint.kind === 'terminalReviewAccepted')
|
|
51
|
-
return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
|
|
69
|
+
return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
|
|
70
|
+
? { valid: true }
|
|
71
|
+
: { valid: false, reasonCode: 'terminalReviewIncomplete' };
|
|
52
72
|
const l1 = plan.steps.find((step) => step.id === checkpoint.l1Id);
|
|
53
73
|
if (!l1)
|
|
54
|
-
return false;
|
|
74
|
+
return { valid: false, reasonCode: 'l1NotFound' };
|
|
55
75
|
const position = `L${plan.steps.indexOf(l1) + 1}`;
|
|
56
76
|
if (checkpoint.kind === 'l2Completed') {
|
|
57
77
|
const l2 = l1.children.find((step) => step.id === checkpoint.l2Id);
|
|
58
78
|
if (!l2)
|
|
59
|
-
return false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
return { valid: false, reasonCode: 'l2NotFound' };
|
|
80
|
+
const expectedPosition = `${position}.${l1.children.indexOf(l2) + 1}`;
|
|
81
|
+
if (checkpoint.position !== expectedPosition)
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
reasonCode: 'checkpointPositionMismatch',
|
|
85
|
+
expected: { position: expectedPosition },
|
|
86
|
+
observed: { position: checkpoint.position },
|
|
87
|
+
};
|
|
88
|
+
if (l2.state !== 'DONE')
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
reasonCode: 'l2NotDone',
|
|
92
|
+
expected: { l2State: 'DONE' },
|
|
93
|
+
observed: { l2State: l2.state },
|
|
94
|
+
};
|
|
95
|
+
return l1.reviewStatus === 'UNREVIEWED'
|
|
96
|
+
? { valid: true }
|
|
97
|
+
: { valid: false, reasonCode: 'l1AlreadyReviewed' };
|
|
63
98
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
99
|
+
if (checkpoint.position !== position)
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
reasonCode: 'checkpointPositionMismatch',
|
|
103
|
+
expected: { position },
|
|
104
|
+
observed: { position: checkpoint.position },
|
|
105
|
+
};
|
|
106
|
+
if (l1.state !== 'DONE')
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
reasonCode: 'l1NotDone',
|
|
110
|
+
expected: { l1State: 'DONE' },
|
|
111
|
+
observed: { l1State: l1.state },
|
|
112
|
+
};
|
|
113
|
+
if (l1.reviewStatus !== 'REVIEWED')
|
|
114
|
+
return {
|
|
115
|
+
valid: false,
|
|
116
|
+
reasonCode: 'l1NotReviewed',
|
|
117
|
+
expected: { reviewStatus: 'REVIEWED' },
|
|
118
|
+
observed: { reviewStatus: l1.reviewStatus ?? null },
|
|
119
|
+
};
|
|
120
|
+
const incompleteChild = l1.children.find((child) => child.state !== 'DONE');
|
|
121
|
+
if (incompleteChild)
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
reasonCode: 'l1ChildrenIncomplete',
|
|
125
|
+
expected: { childrenDone: true },
|
|
126
|
+
observed: { childrenDone: false, childState: incompleteChild.state },
|
|
127
|
+
};
|
|
128
|
+
if (input.hasActiveL1Writer(position))
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
reasonCode: 'executorStillActive',
|
|
132
|
+
expected: { executorTerminal: true },
|
|
133
|
+
observed: { executorTerminal: false },
|
|
134
|
+
};
|
|
135
|
+
return { valid: true };
|
|
68
136
|
}
|
|
@@ -8,7 +8,11 @@ export class SupervisedPlanRegistry {
|
|
|
8
8
|
plans = new Map();
|
|
9
9
|
accept(sessionId, update) {
|
|
10
10
|
if (update.kind === 'updated')
|
|
11
|
-
this.plans.set(sessionId, {
|
|
11
|
+
this.plans.set(sessionId, {
|
|
12
|
+
plan: update.plan,
|
|
13
|
+
identity: update.identity,
|
|
14
|
+
publicationReason: update.reason,
|
|
15
|
+
});
|
|
12
16
|
}
|
|
13
17
|
find(sessionId) {
|
|
14
18
|
return this.plans.get(sessionId)?.plan ?? null;
|
|
@@ -16,6 +20,9 @@ export class SupervisedPlanRegistry {
|
|
|
16
20
|
identity(sessionId) {
|
|
17
21
|
return this.plans.get(sessionId)?.identity ?? null;
|
|
18
22
|
}
|
|
23
|
+
publicationReason(sessionId) {
|
|
24
|
+
return this.plans.get(sessionId)?.publicationReason ?? null;
|
|
25
|
+
}
|
|
19
26
|
clear(sessionId) {
|
|
20
27
|
this.plans.delete(sessionId);
|
|
21
28
|
}
|
|
@@ -40,7 +40,8 @@ export function registerListAvailableSkills(app, deps) {
|
|
|
40
40
|
return {
|
|
41
41
|
source: project ? 'project' : 'native',
|
|
42
42
|
errors: discovered.errors.map(({ message }) => ({ message })),
|
|
43
|
-
skills:
|
|
43
|
+
skills: skills
|
|
44
|
+
.filter((skill) => !isAlwaysAdvertisedSkill(skill))
|
|
44
45
|
.sort((left, right) => left.path.localeCompare(right.path))
|
|
45
46
|
.map((skill) => ({
|
|
46
47
|
name: skill.name,
|
|
@@ -56,7 +56,10 @@ export const availableSkillSchema = z
|
|
|
56
56
|
.strict();
|
|
57
57
|
/** Gestalt workflow skills are session infrastructure, not optional profile entries. */
|
|
58
58
|
export function isAlwaysAdvertisedSkill(skill) {
|
|
59
|
-
return skill.name
|
|
59
|
+
return isGestaltSkillName(skill.name);
|
|
60
|
+
}
|
|
61
|
+
export function isGestaltSkillName(name) {
|
|
62
|
+
return name.startsWith('gestalt:');
|
|
60
63
|
}
|
|
61
64
|
const profileDocumentSchema = z
|
|
62
65
|
.object({
|
|
@@ -195,8 +198,6 @@ export function reconcileSkillSelectionSnapshot(discovered, selection) {
|
|
|
195
198
|
*/
|
|
196
199
|
export function compileSkillOverride(input) {
|
|
197
200
|
const effective = selectEffectiveSkillSelection(input);
|
|
198
|
-
if (effective.selection === undefined)
|
|
199
|
-
return { source: 'native', skillsConfig: undefined, warnings: [] };
|
|
200
201
|
const reconciled = reconcileSkillSelectionSnapshot(input.discovered, effective.selection);
|
|
201
202
|
return {
|
|
202
203
|
source: effective.source,
|
|
@@ -211,7 +212,10 @@ export function createSkillProfile(input) {
|
|
|
211
212
|
return {
|
|
212
213
|
version: 1,
|
|
213
214
|
name: normalizeSkillProfileName(input.name),
|
|
214
|
-
skills
|
|
215
|
+
// Gestalt skills are mandatory thread infrastructure. Legacy profiles are
|
|
216
|
+
// normalized on read so they cannot pin a stale plugin-cache version or
|
|
217
|
+
// expose a meaningless enable/disable control in the editor.
|
|
218
|
+
skills: createSkillSelection(input.skills.filter((skill) => !isGestaltSkillName(skill.name))),
|
|
215
219
|
};
|
|
216
220
|
}
|
|
217
221
|
/** Parse the version-1 YAML document without performing filesystem access. */
|
|
@@ -3,16 +3,6 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
export function profileAppServerCommand(
|
|
7
|
-
|
|
8
|
-
return skillsConfig === undefined
|
|
9
|
-
? base
|
|
10
|
-
: {
|
|
11
|
-
...base,
|
|
12
|
-
args: [
|
|
13
|
-
...base.args,
|
|
14
|
-
'--config',
|
|
15
|
-
`skills.config = [${skillsConfig.map((entry) => `{ path = ${JSON.stringify(entry.path)}, enabled = ${entry.enabled} }`).join(', ')}]`,
|
|
16
|
-
],
|
|
17
|
-
};
|
|
6
|
+
export function profileAppServerCommand() {
|
|
7
|
+
return { command: 'codex', args: ['app-server', '--stdio'] };
|
|
18
8
|
}
|
|
@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
import { JsonRpcClient } from './json-rpc-client.js';
|
|
8
8
|
import { profileAppServerCommand } from '../catalog/profile-command.js';
|
|
9
9
|
export function launchCodexAppServer(input) {
|
|
10
|
-
const launch = profileAppServerCommand(
|
|
10
|
+
const launch = profileAppServerCommand();
|
|
11
11
|
const child = spawn(launch.command, launch.args, {
|
|
12
12
|
cwd: input.cwd,
|
|
13
13
|
env: codexChildEnvironment(input.environment),
|
|
@@ -17,10 +17,15 @@ import { threadPlanName } from './thread-plan-name.js';
|
|
|
17
17
|
import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
|
|
18
18
|
import { isCodexThreadWriterBusy, isMissingCodexThreadRollout } from './json-rpc-client.js';
|
|
19
19
|
import { resolvedServerRequestId } from './server-request.js';
|
|
20
|
+
/** Sticky thread config keeps skill paths out of the app-server process argv. */
|
|
21
|
+
export function threadSkillConfig(skillsConfig) {
|
|
22
|
+
return skillsConfig === undefined ? {} : { config: { skills: { config: skillsConfig } } };
|
|
23
|
+
}
|
|
20
24
|
/** One private owner for every resource acquired for a live Codex child. */
|
|
21
25
|
class SessionResource {
|
|
22
26
|
sessionId;
|
|
23
27
|
process;
|
|
28
|
+
skillsConfig;
|
|
24
29
|
planStatusLease;
|
|
25
30
|
planMeasurementToken;
|
|
26
31
|
unregister;
|
|
@@ -39,9 +44,10 @@ class SessionResource {
|
|
|
39
44
|
completedProcessActions = new Map();
|
|
40
45
|
turnThreads = new Map();
|
|
41
46
|
childThreads = new Set();
|
|
42
|
-
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
47
|
+
constructor(sessionId, process, skillsConfig, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
43
48
|
this.sessionId = sessionId;
|
|
44
49
|
this.process = process;
|
|
50
|
+
this.skillsConfig = skillsConfig;
|
|
45
51
|
this.planStatusLease = planStatusLease;
|
|
46
52
|
this.planMeasurementToken = planMeasurementToken;
|
|
47
53
|
this.unregister = unregister;
|
|
@@ -117,7 +123,7 @@ export class CodexSessionRuntime {
|
|
|
117
123
|
clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
|
|
118
124
|
capabilities: { experimentalApi: true },
|
|
119
125
|
});
|
|
120
|
-
const startedThreadId = await this.startThread(resource.process, session);
|
|
126
|
+
const startedThreadId = await this.startThread(resource.process, session, resource.skillsConfig);
|
|
121
127
|
resource.threadId = startedThreadId;
|
|
122
128
|
this.sessions.set(session.id, resource);
|
|
123
129
|
await this.writePendingThreadName(session.id);
|
|
@@ -671,6 +677,7 @@ export class CodexSessionRuntime {
|
|
|
671
677
|
await resource.process.rpc.request('thread/resume', {
|
|
672
678
|
threadId: session.threadId,
|
|
673
679
|
cwd: session.workspacePath,
|
|
680
|
+
...threadSkillConfig(resource.skillsConfig),
|
|
674
681
|
...(session.executionPolicy?.approvalPolicy
|
|
675
682
|
? { approvalPolicy: session.executionPolicy.approvalPolicy }
|
|
676
683
|
: {}),
|
|
@@ -692,7 +699,7 @@ export class CodexSessionRuntime {
|
|
|
692
699
|
catch (error) {
|
|
693
700
|
if (!canRebindMissingRollout(session, error))
|
|
694
701
|
throw error;
|
|
695
|
-
const replacementThreadId = await this.startThread(resource.process, session);
|
|
702
|
+
const replacementThreadId = await this.startThread(resource.process, session, resource.skillsConfig);
|
|
696
703
|
result = rebindMissingRollout(session, error, replacementThreadId, now);
|
|
697
704
|
}
|
|
698
705
|
resource.threadId = result.session.threadId;
|
|
@@ -760,12 +767,13 @@ export class CodexSessionRuntime {
|
|
|
760
767
|
reject(new Error('CODEX_SERVER_REQUEST_UNSUPPORTED'));
|
|
761
768
|
});
|
|
762
769
|
}
|
|
763
|
-
async startThread(process, session) {
|
|
764
|
-
return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session)));
|
|
770
|
+
async startThread(process, session, skillsConfig) {
|
|
771
|
+
return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session, skillsConfig)));
|
|
765
772
|
}
|
|
766
|
-
threadStartParams(session) {
|
|
773
|
+
threadStartParams(session, skillsConfig) {
|
|
767
774
|
return {
|
|
768
775
|
cwd: session.workspacePath,
|
|
776
|
+
...threadSkillConfig(skillsConfig),
|
|
769
777
|
approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
|
|
770
778
|
dynamicTools: [
|
|
771
779
|
gestaltQuizDynamicTool,
|
|
@@ -792,10 +800,10 @@ export class CodexSessionRuntime {
|
|
|
792
800
|
: undefined;
|
|
793
801
|
try {
|
|
794
802
|
const token = randomUUID();
|
|
803
|
+
const skillsConfig = await this.resolveSkills?.(session);
|
|
795
804
|
const process = this.launch({
|
|
796
805
|
profile: session.profile,
|
|
797
806
|
cwd: session.workspacePath,
|
|
798
|
-
skillsConfig: await this.resolveSkills?.(session),
|
|
799
807
|
...(lease || this.planMeasurementBaseUrl
|
|
800
808
|
? {
|
|
801
809
|
environment: {
|
|
@@ -812,7 +820,7 @@ export class CodexSessionRuntime {
|
|
|
812
820
|
}
|
|
813
821
|
: {}),
|
|
814
822
|
});
|
|
815
|
-
const resource = new SessionResource(session.id, process, lease, token, [], () => {
|
|
823
|
+
const resource = new SessionResource(session.id, process, skillsConfig, lease, token, [], () => {
|
|
816
824
|
if (this.sessions.get(session.id) === resource)
|
|
817
825
|
this.sessions.delete(session.id);
|
|
818
826
|
});
|
|
@@ -17,16 +17,22 @@ export class SqlitePendingInteractionStore {
|
|
|
17
17
|
constructor(db) {
|
|
18
18
|
this.db = db;
|
|
19
19
|
}
|
|
20
|
-
add(sessionId, interaction) {
|
|
20
|
+
add(sessionId, interaction, initialResolutionState = 'active') {
|
|
21
21
|
this.db
|
|
22
|
-
.prepare(`INSERT INTO pending_interactions (session_id,request_id,kind,payload_json,turn_id,requested_at)
|
|
23
|
-
VALUES (
|
|
22
|
+
.prepare(`INSERT INTO pending_interactions (session_id,request_id,kind,payload_json,turn_id,requested_at,resolution_state)
|
|
23
|
+
VALUES (?,?,?,?,?,?,?)
|
|
24
24
|
ON CONFLICT(session_id,request_id) DO UPDATE SET
|
|
25
25
|
kind = excluded.kind, payload_json = excluded.payload_json, turn_id = excluded.turn_id,
|
|
26
26
|
requested_at = excluded.requested_at, resolved_at = NULL, outcome = NULL, operation_key = NULL,
|
|
27
|
-
resolution_state =
|
|
27
|
+
resolution_state = excluded.resolution_state
|
|
28
28
|
WHERE pending_interactions.kind != 'orgPlanAttention'`)
|
|
29
|
-
.run(sessionId, interaction.requestId, interaction.kind, JSON.stringify(interaction.payload), interaction.turnId ?? null, interaction.requestedAt ?? null);
|
|
29
|
+
.run(sessionId, interaction.requestId, interaction.kind, JSON.stringify(interaction.payload), interaction.turnId ?? null, interaction.requestedAt ?? null, initialResolutionState);
|
|
30
|
+
}
|
|
31
|
+
resolutionState(sessionId, requestId) {
|
|
32
|
+
const row = this.db
|
|
33
|
+
.prepare('SELECT resolution_state FROM pending_interactions WHERE session_id = ? AND request_id = ? AND resolved_at IS NULL')
|
|
34
|
+
.get(sessionId, requestId);
|
|
35
|
+
return row?.resolution_state ?? null;
|
|
30
36
|
}
|
|
31
37
|
resolve(sessionId, requestId, resolvedAt, outcome = 'answered') {
|
|
32
38
|
return (this.db
|
|
@@ -56,7 +62,7 @@ export class SqlitePendingInteractionStore {
|
|
|
56
62
|
/** Persist the in-flight delivery boundary before the app-server is notified. */
|
|
57
63
|
beginDelivery(sessionId, requestId, operationKey) {
|
|
58
64
|
return (this.db
|
|
59
|
-
.prepare("UPDATE pending_interactions SET resolution_state = 'delivering' WHERE session_id = ? AND request_id = ? AND operation_key = ? AND resolved_at IS NULL AND resolution_state IN ('active','delivering')")
|
|
65
|
+
.prepare("UPDATE pending_interactions SET resolution_state = 'delivering' WHERE session_id = ? AND request_id = ? AND operation_key = ? AND resolved_at IS NULL AND resolution_state IN ('active','acknowledged','delivering')")
|
|
60
66
|
.run(sessionId, requestId, operationKey).changes === 1);
|
|
61
67
|
}
|
|
62
68
|
/** A local writer outage is retryable only by the already-claimed key. */
|