gestalt-mobile 0.41.2 → 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-9uZwieND.js → index-BRly-q56.js} +23 -23
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +9 -13
- package/dist/server/server/composition.js +27 -3
- 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/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/shared/contracts/org-plan-attention.js +35 -6
- package/package.json +1 -1
package/dist/client/index.html
CHANGED
|
@@ -16,7 +16,7 @@ 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-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BRly-q56.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-D57mr3B1.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
@@ -12,7 +12,7 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { describe, expect, it, onTestFinished, vi } from 'vitest';
|
|
13
13
|
import WebSocket from 'ws';
|
|
14
14
|
import { composeRelayApp } from './composition.js';
|
|
15
|
-
import {
|
|
15
|
+
import { autopilotExecutorLaunchPrompt } from './features/autopilot/application/policy.js';
|
|
16
16
|
import { CodexJsonRpcError } from './platform/codex/json-rpc-client.js';
|
|
17
17
|
import { SqliteAuthorizationStore } from './platform/auth/sqlite-authorization-store.js';
|
|
18
18
|
import { authorizationSessionId, authorizedDeviceId, localOwnerId, webAuthnCredentialId, } from './features/auth/domain/identifiers.js';
|
|
@@ -497,7 +497,12 @@ describe('production composition', () => {
|
|
|
497
497
|
input: [
|
|
498
498
|
{
|
|
499
499
|
type: 'text',
|
|
500
|
-
text:
|
|
500
|
+
text: autopilotExecutorLaunchPrompt({
|
|
501
|
+
canonicalTaskName: 'l1',
|
|
502
|
+
canonicalPosition: 'L1',
|
|
503
|
+
generation: 1,
|
|
504
|
+
taskName: 'l1',
|
|
505
|
+
}),
|
|
501
506
|
text_elements: [],
|
|
502
507
|
},
|
|
503
508
|
],
|
|
@@ -1455,7 +1460,7 @@ describe('production composition', () => {
|
|
|
1455
1460
|
.filter((request) => request.method === 'turn/start')).toHaveLength(startsAfterFinal);
|
|
1456
1461
|
await fixture.app.close();
|
|
1457
1462
|
});
|
|
1458
|
-
it('production attention
|
|
1463
|
+
it('production attention approval resumes Autopilot and a complete plan transitions to completed', async () => {
|
|
1459
1464
|
const fixture = await createProductionAutopilotFixture();
|
|
1460
1465
|
const handle = fixture.handles.find((candidate) => candidate.request);
|
|
1461
1466
|
expect((await fixture.app.inject({
|
|
@@ -1474,15 +1479,6 @@ describe('production composition', () => {
|
|
|
1474
1479
|
url: `/api/sessions/${fixture.sessionId}/attention/810/resolve`,
|
|
1475
1480
|
payload: { operationKey: 'resume-attention', action: 'resume' },
|
|
1476
1481
|
})).statusCode).toBe(202);
|
|
1477
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1478
|
-
expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1479
|
-
autopilot: { state: 'attentionRequired', enabled: false },
|
|
1480
|
-
});
|
|
1481
|
-
expect((await fixture.app.inject({
|
|
1482
|
-
method: 'PUT',
|
|
1483
|
-
url: `/api/sessions/${fixture.sessionId}/autopilot`,
|
|
1484
|
-
payload: { enabled: true },
|
|
1485
|
-
})).statusCode).toBe(200);
|
|
1486
1482
|
await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
|
|
1487
1483
|
autopilot: { enabled: true, state: expect.stringMatching(/monitoring|backoff/) },
|
|
1488
1484
|
}));
|
|
@@ -1770,7 +1766,7 @@ describe('production composition', () => {
|
|
|
1770
1766
|
// rejected child continuation; it must never target the dead child.
|
|
1771
1767
|
expect((starts[3]?.params).threadId).not.toBe('child-1');
|
|
1772
1768
|
expect((starts[3]?.params).input?.[0]?.text).toContain('Launch task_name l1_g2 for canonical L1');
|
|
1773
|
-
expect((starts[3]?.params).input?.[0]?.text).toContain('
|
|
1769
|
+
expect((starts[3]?.params).input?.[0]?.text).toContain('agent_type org-plan-executor and reasoning_effort high');
|
|
1774
1770
|
// Capacity recovery is allowed only for the active root control. It
|
|
1775
1771
|
// recycles that writer but must retain the one durable g2 handoff.
|
|
1776
1772
|
const rootHandle = fixture.handles.find((candidate) => candidate.requests.some((request) => request.method === 'turn/start' &&
|
|
@@ -66,7 +66,7 @@ import { AutopilotCoordinator } from './features/autopilot/application/service.j
|
|
|
66
66
|
import { deriveSessionStatus } from './features/sessions/session-status.js';
|
|
67
67
|
import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, autopilotExecutorLaunchPrompt, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
68
68
|
import { createRelyingPartyConfig } from './config.js';
|
|
69
|
-
import { parseOrgPlanAttention, toOrgPlanAttentionAcknowledgement, } from '../shared/contracts/org-plan-attention.js';
|
|
69
|
+
import { parseOrgPlanAttention, parseOrgPlanAttentionToolResponse, toOrgPlanAttentionAcknowledgement, } from '../shared/contracts/org-plan-attention.js';
|
|
70
70
|
import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
|
|
71
71
|
import { agentCapacityRecoveryToolResponse } from '../shared/contracts/agent-capacity-recovery.js';
|
|
72
72
|
import { pwaIconUrl } from './features/pwa/register-routes.js';
|
|
@@ -400,8 +400,6 @@ export async function composeRelayApp(options) {
|
|
|
400
400
|
explicit: session.effectiveSkillSelection.skills,
|
|
401
401
|
}).skillsConfig;
|
|
402
402
|
const project = await skillProfiles.readWorkspaceDefault(session.workspacePath);
|
|
403
|
-
if (!options.explicitSkillProfile && !project)
|
|
404
|
-
return undefined;
|
|
405
403
|
return compileSkillOverride({
|
|
406
404
|
discovered: catalog.skills,
|
|
407
405
|
explicit: options.explicitSkillProfile?.skills,
|
|
@@ -1433,6 +1431,10 @@ export async function composeRelayApp(options) {
|
|
|
1433
1431
|
const interaction = interactions.find(sessionId, requestId);
|
|
1434
1432
|
if (!interaction || interaction.kind !== 'orgPlanAttention')
|
|
1435
1433
|
return { kind: 'noActive' };
|
|
1434
|
+
const decision = parseOrgPlanAttentionToolResponse(response);
|
|
1435
|
+
const attention = parseOrgPlanAttention(interaction.payload);
|
|
1436
|
+
if (!decision || !attention)
|
|
1437
|
+
return { kind: 'staleOperation' };
|
|
1436
1438
|
const claim = interactions.claimOperation(sessionId, requestId, operationKey);
|
|
1437
1439
|
if (claim === 'resolved') {
|
|
1438
1440
|
const resolved = interactions.resolved(sessionId, requestId);
|
|
@@ -1448,12 +1450,23 @@ export async function composeRelayApp(options) {
|
|
|
1448
1450
|
if (acknowledged) {
|
|
1449
1451
|
if (!interactions.beginDelivery(sessionId, requestId, operationKey))
|
|
1450
1452
|
return { kind: 'staleOperation' };
|
|
1453
|
+
if (decision.action === 'resume' && attention.executorReplacement) {
|
|
1454
|
+
const authorization = autopilot.authorizeExecutorReplacement(sessionId, attention.executorReplacement.canonicalTaskName);
|
|
1455
|
+
if (!authorization.accepted) {
|
|
1456
|
+
interactions.retryDelivery(sessionId, requestId, operationKey);
|
|
1457
|
+
return { kind: 'replacementRejected' };
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1451
1460
|
const resolvedAt = new Date().toISOString();
|
|
1452
1461
|
if (!interactions.settleOperation(sessionId, requestId, operationKey, resolvedAt, 'answered'))
|
|
1453
1462
|
return { kind: 'staleOperation' };
|
|
1454
1463
|
const accepted = { kind: 'accepted', resolvedAt };
|
|
1455
1464
|
idempotency.put(scope, operationKey, 202, JSON.stringify({ kind: 'replayed', resolvedAt }));
|
|
1456
1465
|
publishAttentionSettlement(sessionId, requestId, resolvedAt, 'answered');
|
|
1466
|
+
if (decision.action === 'disableAutopilot')
|
|
1467
|
+
autopilot.disable(sessionId);
|
|
1468
|
+
else
|
|
1469
|
+
autopilot.enable(sessionId);
|
|
1457
1470
|
return accepted;
|
|
1458
1471
|
}
|
|
1459
1472
|
// A durable capability belongs to the session/thread, not the
|
|
@@ -1465,6 +1478,13 @@ export async function composeRelayApp(options) {
|
|
|
1465
1478
|
return { kind: 'writerUnavailable' };
|
|
1466
1479
|
if (!interactions.beginDelivery(sessionId, requestId, operationKey))
|
|
1467
1480
|
return { kind: 'staleOperation' };
|
|
1481
|
+
if (decision.action === 'resume' && attention.executorReplacement) {
|
|
1482
|
+
const authorization = autopilot.authorizeExecutorReplacement(sessionId, attention.executorReplacement.canonicalTaskName);
|
|
1483
|
+
if (!authorization.accepted) {
|
|
1484
|
+
interactions.retryDelivery(sessionId, requestId, operationKey);
|
|
1485
|
+
return { kind: 'replacementRejected' };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1468
1488
|
const writer = runtime.attentionWriterState(sessionId, requestId);
|
|
1469
1489
|
if (writer === 'unavailable') {
|
|
1470
1490
|
interactions.retryDelivery(sessionId, requestId, operationKey);
|
|
@@ -1488,6 +1508,10 @@ export async function composeRelayApp(options) {
|
|
|
1488
1508
|
const accepted = { kind: 'accepted', resolvedAt };
|
|
1489
1509
|
idempotency.put(scope, operationKey, 202, JSON.stringify({ kind: 'replayed', resolvedAt }));
|
|
1490
1510
|
publishAttentionSettlement(sessionId, requestId, resolvedAt, 'answered');
|
|
1511
|
+
if (decision.action === 'disableAutopilot')
|
|
1512
|
+
autopilot.disable(sessionId);
|
|
1513
|
+
else
|
|
1514
|
+
autopilot.enable(sessionId);
|
|
1491
1515
|
return accepted;
|
|
1492
1516
|
})();
|
|
1493
1517
|
attentionResolutionOperations.set(key, operation);
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
export const AUTOPILOT_PROMPT_VERSION = '
|
|
7
|
-
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. Roll up commentary, files, verification, and commands since the previous boundary into that one answer; all later milestone activity belongs to a new root turn. On this later Autopilot turn, resume the same executor after an L2 boundary or launch the next
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v13';
|
|
7
|
+
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. An accepted L1 always ends the root turn with a chat answer: never continue into the next L1 or emit post-acceptance documentation as commentary. Roll up commentary, files, verification, and commands since the previous boundary into that one answer; all later milestone activity belongs to a new root turn. On this later Autopilot turn, resume the same executor after an L2 boundary or launch exactly one fresh canonical executor for the next L1 after an accepted-L1 boundary. Treat a status question as an interruption: answer briefly, then perform the applicable continuation in the same turn. Before sending any blocker response or yielding because progress cannot continue safely, call gestalt_org_plan_attention for the matching decision-table blocker; never rely on blocker prose as the signal. When explicit human permission is required for a physical executor replacement, include executorReplacement with the exact canonicalTaskName; Mobile alone computes and persists the physical generation. Do not call attention for routine progress or a recoverable failure. Before yielding for known long work, call gestalt_autopilot_wait_lease version 2 with relevant observable wake conditions and a bounded maxWaitMs; it is one episode, so reassess before registering another in a later turn. Yield only when its response contains accepted:true; accepted:false means no continuation was registered and requires same-turn supervision. For GitHub PR CI, start an owned gh pr checks --watch process and lease processExited plus processResultAvailable while it runs. When the Autopilot probe is active, register the compatible wait lease, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
|
|
8
8
|
export const AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT = 'Continue the same assigned Org L1 from its durable state. A prior turn ending did not complete the objective. Consume any supplied process result and take the next legal L2 action. Whenever an L2 reaches DONE, return its concise structured evidence and end the executor turn so the root can publish that mandatory answer boundary. Also report at the L1 review boundary or through structured attention.';
|
|
9
9
|
/** Builds a physical launch instruction while keeping durable L1 identity canonical. */
|
|
10
10
|
export function autopilotExecutorLaunchPrompt(identity) {
|
|
11
|
-
const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition};
|
|
11
|
+
const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition} with agent_type org-plan-executor and reasoning_effort high; the profile selects gpt-5.6-terra by default, so do not override its model. Retain the canonical label in status and review output.`;
|
|
12
12
|
if (identity.generation === 1)
|
|
13
13
|
return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch}`;
|
|
14
|
-
return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This exact replacement generation ${identity.generation} is durably authorized by Mobile; do not infer another generation from this prompt or the roster; the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn ${identity.taskName}
|
|
14
|
+
return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This exact replacement generation ${identity.generation} is durably authorized by Mobile; do not infer another generation from this prompt or the roster; the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn the authorized ${identity.taskName}. Transfer sole ${identity.canonicalPosition} ownership and continue from its durable Org Plan and worktree state.`;
|
|
15
15
|
}
|
|
16
16
|
export const defaultAutopilotPolicy = Object.freeze({
|
|
17
17
|
quiescenceMs: 1_000,
|
|
@@ -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
|
}
|
|
@@ -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
|
});
|