gestalt-mobile 0.41.0 → 0.41.1
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/server/server/composition.contracts.js +19 -9
- package/dist/server/server/composition.js +11 -11
- package/dist/server/server/features/autopilot/application/policy.js +2 -2
- package/dist/server/server/features/autopilot/application/service.js +93 -29
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +81 -7
- package/dist/server/shared/contracts/org-plan-checkpoint.js +23 -3
- package/package.json +1 -1
|
@@ -789,13 +789,21 @@ describe('production composition', () => {
|
|
|
789
789
|
beforeWriteDatabase.close();
|
|
790
790
|
expect(beforeWrite.resolved_at).toBeNull();
|
|
791
791
|
handle.responseSettled({ id: 811, outcome: 'resultWritten' });
|
|
792
|
-
await expect(checkpointResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
792
|
+
await expect(checkpointResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
793
|
+
const replayResponse = handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity));
|
|
794
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(812)).toBe(true));
|
|
795
|
+
handle.responseSettled({ id: 812, outcome: 'resultWritten' });
|
|
796
|
+
await expect(replayResponse).resolves.toEqual(toOrgPlanCheckpointToolResponse('alreadyRecorded'));
|
|
793
797
|
const afterWriteDatabase = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
794
798
|
const afterWrite = afterWriteDatabase
|
|
795
799
|
.prepare('SELECT resolved_at FROM pending_interactions WHERE session_id = ? AND request_id = ?')
|
|
796
800
|
.get(fixture.sessionId, '811');
|
|
801
|
+
const checkpointEvents = afterWriteDatabase
|
|
802
|
+
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
|
|
803
|
+
.get(fixture.sessionId);
|
|
797
804
|
afterWriteDatabase.close();
|
|
798
805
|
expect(afterWrite.resolved_at).toEqual(expect.any(String));
|
|
806
|
+
expect(checkpointEvents.count).toBe(1);
|
|
799
807
|
expect(timers).toHaveLength(schedulesBeforeCheckpoint);
|
|
800
808
|
expect(coordinator.snapshot(fixture.sessionId).health).not.toMatchObject({
|
|
801
809
|
phase: 'waitingForAgentEvent',
|
|
@@ -941,7 +949,7 @@ describe('production composition', () => {
|
|
|
941
949
|
});
|
|
942
950
|
const { handle, response } = await holdCheckpointRequest(fixture, timers, 812, true, true);
|
|
943
951
|
handle.responseSettled({ id: 812, outcome: 'resultWritten' });
|
|
944
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
952
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
945
953
|
const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
|
|
946
954
|
const checkpoint = database
|
|
947
955
|
.prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
|
|
@@ -950,7 +958,7 @@ describe('production composition', () => {
|
|
|
950
958
|
expect(checkpoint.count).toBe(1);
|
|
951
959
|
await fixture.app.close();
|
|
952
960
|
});
|
|
953
|
-
it('
|
|
961
|
+
it('returns a typed failed checkpoint result when persistence never occurs', async () => {
|
|
954
962
|
let coordinator;
|
|
955
963
|
const timers = [];
|
|
956
964
|
const fixture = await createProductionAutopilotFixture({
|
|
@@ -969,8 +977,10 @@ describe('production composition', () => {
|
|
|
969
977
|
vi.spyOn(coordinator, 'checkpointAccepted').mockImplementation(() => {
|
|
970
978
|
throw new Error('injected-before-checkpoint-persistence');
|
|
971
979
|
});
|
|
972
|
-
const { response, startsBefore, writersBefore } = await holdCheckpointRequest(fixture, timers, 995, false);
|
|
973
|
-
await expect(
|
|
980
|
+
const { handle, response, startsBefore, writersBefore } = await holdCheckpointRequest(fixture, timers, 995, false);
|
|
981
|
+
await vi.waitFor(() => expect(handle.pendingResponses.has(995)).toBe(true));
|
|
982
|
+
handle.responseSettled({ id: 995, outcome: 'resultWritten' });
|
|
983
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('failed'));
|
|
974
984
|
expect(coordinator.checkpointAccepted).not.toBe(original);
|
|
975
985
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
976
986
|
expect(fixture.handles
|
|
@@ -1139,7 +1149,7 @@ describe('production composition', () => {
|
|
|
1139
1149
|
};
|
|
1140
1150
|
if (scenario === 'resultWritten-before-timeout') {
|
|
1141
1151
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1142
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
1152
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1143
1153
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1144
1154
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
1145
1155
|
expect(failures()).toBe(0);
|
|
@@ -1147,7 +1157,7 @@ describe('production composition', () => {
|
|
|
1147
1157
|
else if (scenario === 'duplicate-settlement') {
|
|
1148
1158
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1149
1159
|
handle.responseSettled({ id: 992, outcome: 'resultWritten' });
|
|
1150
|
-
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
|
|
1160
|
+
await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse('recorded'));
|
|
1151
1161
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1152
1162
|
expect(fixture.handles).toHaveLength(writersBefore);
|
|
1153
1163
|
expect(failures()).toBe(0);
|
|
@@ -1292,7 +1302,7 @@ describe('production composition', () => {
|
|
|
1292
1302
|
expect(approvalTurn.statusCode).toBe(202);
|
|
1293
1303
|
await approvalFixture.app.close();
|
|
1294
1304
|
});
|
|
1295
|
-
it('
|
|
1305
|
+
it('returns failed for a checkpoint delivered after its owning root final has closed', async () => {
|
|
1296
1306
|
const timers = [];
|
|
1297
1307
|
const fixture = await createProductionAutopilotFixture({
|
|
1298
1308
|
autopilotSchedule: (callback) => {
|
|
@@ -1330,7 +1340,7 @@ describe('production composition', () => {
|
|
|
1330
1340
|
const planIdentity = createHash('sha256')
|
|
1331
1341
|
.update(join(fixture.workspacePath, 'autopilot.org'))
|
|
1332
1342
|
.digest('hex');
|
|
1333
|
-
await expect(handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity))).
|
|
1343
|
+
await expect(handle.request(l2CheckpointCall(812, threadId, active.active_turn_id, planIdentity))).resolves.toEqual(toOrgPlanCheckpointToolResponse('failed'));
|
|
1334
1344
|
expect(timers).toHaveLength(timersAfterFinal);
|
|
1335
1345
|
expect(fixture.handles
|
|
1336
1346
|
.flatMap((candidate) => candidate.requests)
|
|
@@ -741,7 +741,7 @@ export async function composeRelayApp(options) {
|
|
|
741
741
|
!identity ||
|
|
742
742
|
checkpoint.planIdentity !== identity ||
|
|
743
743
|
!rootOwned)
|
|
744
|
-
return
|
|
744
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('failed')) === true);
|
|
745
745
|
const valid = validOrgPlanCheckpoint({
|
|
746
746
|
checkpoint,
|
|
747
747
|
plan: retained,
|
|
@@ -749,17 +749,15 @@ export async function composeRelayApp(options) {
|
|
|
749
749
|
rootOwned,
|
|
750
750
|
hasActiveL1Writer: (position) => hasActiveL1Writer(activity.snapshot(sessionId, new Date().toISOString()), position),
|
|
751
751
|
});
|
|
752
|
-
if (!valid)
|
|
753
|
-
return
|
|
754
|
-
|
|
755
|
-
return false;
|
|
756
|
-
let accepted = false;
|
|
752
|
+
if (!valid || !session.activeTurnId)
|
|
753
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('failed')) === true);
|
|
754
|
+
let recordStatus = 'failed';
|
|
757
755
|
try {
|
|
758
|
-
|
|
756
|
+
recordStatus = autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString());
|
|
759
757
|
}
|
|
760
758
|
catch {
|
|
761
759
|
if (!autopilot.checkpointHandoffFailed(sessionId, session.activeTurnId))
|
|
762
|
-
return
|
|
760
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('failed')) === true);
|
|
763
761
|
const interaction = {
|
|
764
762
|
...rawInteraction,
|
|
765
763
|
turnId: session.activeTurnId,
|
|
@@ -769,8 +767,10 @@ export async function composeRelayApp(options) {
|
|
|
769
767
|
failCheckpointHandoff(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
770
768
|
return true;
|
|
771
769
|
}
|
|
772
|
-
if (
|
|
773
|
-
return
|
|
770
|
+
if (recordStatus === 'failed')
|
|
771
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('failed')) === true);
|
|
772
|
+
if (recordStatus === 'alreadyRecorded')
|
|
773
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('alreadyRecorded')) === true);
|
|
774
774
|
const interaction = {
|
|
775
775
|
...rawInteraction,
|
|
776
776
|
turnId: session.activeTurnId,
|
|
@@ -779,7 +779,7 @@ export async function composeRelayApp(options) {
|
|
|
779
779
|
interactions.add(sessionId, interaction);
|
|
780
780
|
armCheckpointHandoffDeadline(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
781
781
|
try {
|
|
782
|
-
runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse());
|
|
782
|
+
runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('recorded'));
|
|
783
783
|
}
|
|
784
784
|
catch {
|
|
785
785
|
// The accepted hold remains owned by the bounded deadline.
|
|
@@ -3,7 +3,7 @@
|
|
|
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 = '
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v11';
|
|
7
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 executor 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. Do not call it 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. */
|
|
@@ -11,7 +11,7 @@ export function autopilotExecutorLaunchPrompt(identity) {
|
|
|
11
11
|
const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition}; 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
|
|
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} with agent_type worker and an explicit model selected by the supervisor. 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,
|
|
@@ -7,7 +7,7 @@ import { deriveSessionStatus } from '../../sessions/session-status.js';
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { AUTOPILOT_WAIT_MAX_MS, AUTOPILOT_WAIT_MIN_MS, } from '../../../../shared/contracts/autopilot-wait-lease.js';
|
|
9
9
|
import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
|
|
10
|
-
import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, checkpointTarget, } from '../domain/supervised-lifecycle.js';
|
|
10
|
+
import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, checkpointTarget, executorAssignment, } from '../domain/supervised-lifecycle.js';
|
|
11
11
|
import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
|
|
12
12
|
import { consumeCheckpointBoundary, consumeObsoleteWait, consumeObservableWake, consumeWaitDeadline, recordAutomaticContinuation, recoverSafetyPause, registerProactiveWait as registerProtocolWait, reportProbe, semanticProgressKey, startSupervisionProtocol, } from '../domain/supervision-protocol.js';
|
|
13
13
|
export class AutopilotCoordinator {
|
|
@@ -213,7 +213,7 @@ export class AutopilotCoordinator {
|
|
|
213
213
|
const activity = this.deps.activity(sessionId);
|
|
214
214
|
if (!retained || activity?.confidence !== 'fresh')
|
|
215
215
|
return false;
|
|
216
|
-
const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor);
|
|
216
|
+
const executor = this.currentExecutor(sessionId, retained.plan, state.consecutiveNoProgress, state.executor, activity);
|
|
217
217
|
return Boolean(executor && !this.authoritativeExecutorActive(state, retained.plan, activity));
|
|
218
218
|
}
|
|
219
219
|
/** Fails closed only for the currently active bounded probe. */
|
|
@@ -612,10 +612,10 @@ export class AutopilotCoordinator {
|
|
|
612
612
|
const prior = this.deps.store.find(sessionId);
|
|
613
613
|
const retained = this.deps.plan(sessionId);
|
|
614
614
|
if (!prior || !retained || !turnId || checkpoint.planIdentity !== retained.identity)
|
|
615
|
-
return
|
|
615
|
+
return 'failed';
|
|
616
616
|
const previous = prior.checkpoints;
|
|
617
617
|
if (previous && previous.planIdentity !== retained.identity)
|
|
618
|
-
return
|
|
618
|
+
return 'failed';
|
|
619
619
|
const reportedL1Ids = previous?.reportedL1Ids ?? [];
|
|
620
620
|
const reportedL2Ids = previous?.reportedL2Ids ?? [];
|
|
621
621
|
const canonicalPosition = checkpoint.kind === 'terminalReviewAccepted' ? 'terminal' : checkpoint.position;
|
|
@@ -631,12 +631,12 @@ export class AutopilotCoordinator {
|
|
|
631
631
|
.digest('hex');
|
|
632
632
|
const acceptedKeys = previous?.acceptedKeys ?? [];
|
|
633
633
|
if (acceptedKeys.includes(key))
|
|
634
|
-
return
|
|
634
|
+
return 'alreadyRecorded';
|
|
635
635
|
const l2Key = checkpoint.kind === 'l2Completed' ? JSON.stringify([checkpoint.l1Id, checkpoint.l2Id]) : null;
|
|
636
636
|
if (checkpoint.kind === 'terminalReviewAccepted' && previous?.terminalReviewAccepted)
|
|
637
|
-
return
|
|
637
|
+
return 'alreadyRecorded';
|
|
638
638
|
if (completionEpoch?.completed && !completionEpoch.reopened)
|
|
639
|
-
return
|
|
639
|
+
return 'alreadyRecorded';
|
|
640
640
|
const completionEpochs = [
|
|
641
641
|
...(previous?.completionEpochs ?? []).filter((entry) => entry.target !== target),
|
|
642
642
|
{ target, epoch, reopened: false, completed: true },
|
|
@@ -655,7 +655,19 @@ export class AutopilotCoordinator {
|
|
|
655
655
|
checkpointHandoffFailed: false,
|
|
656
656
|
terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
|
|
657
657
|
};
|
|
658
|
-
const checkpointed = {
|
|
658
|
+
const checkpointed = {
|
|
659
|
+
...prior,
|
|
660
|
+
...(checkpoint.kind === 'l1Accepted' && prior.executor?.assignment
|
|
661
|
+
? {
|
|
662
|
+
executor: {
|
|
663
|
+
...prior.executor,
|
|
664
|
+
assignment: { ...prior.executor.assignment, state: 'closed' },
|
|
665
|
+
},
|
|
666
|
+
}
|
|
667
|
+
: {}),
|
|
668
|
+
checkpoints,
|
|
669
|
+
updatedAt: occurredAt,
|
|
670
|
+
};
|
|
659
671
|
this.persist({
|
|
660
672
|
...checkpointed,
|
|
661
673
|
supervision: checkpointed.supervision
|
|
@@ -688,7 +700,7 @@ export class AutopilotCoordinator {
|
|
|
688
700
|
this.cancelWaitTimer(sessionId);
|
|
689
701
|
this.parkedSubscriptions.delete(sessionId);
|
|
690
702
|
this.cancelTimer(sessionId);
|
|
691
|
-
return
|
|
703
|
+
return 'recorded';
|
|
692
704
|
}
|
|
693
705
|
/**
|
|
694
706
|
* Records a failed checkpoint transport handoff without releasing its root
|
|
@@ -852,6 +864,12 @@ export class AutopilotCoordinator {
|
|
|
852
864
|
this.activityEventKeys.set(sessionId, eventKey);
|
|
853
865
|
if (this.semanticEvent(sessionId, 'agentActivityChanged'))
|
|
854
866
|
return;
|
|
867
|
+
// `executorChanged` is an edge subscription, not a request to wait for a
|
|
868
|
+
// later generic reconciliation pass. Consume it from the same fresh
|
|
869
|
+
// activity snapshot that exposed the settled canonical executor.
|
|
870
|
+
if (this.executorWakeAlreadySatisfied(sessionId, prior, ['executorChanged']) &&
|
|
871
|
+
this.semanticEvent(sessionId, 'executorChanged'))
|
|
872
|
+
return;
|
|
855
873
|
const disposition = classifyAgentActivity(activity);
|
|
856
874
|
if (disposition === 'attention') {
|
|
857
875
|
this.evaluate(sessionId);
|
|
@@ -1283,23 +1301,36 @@ export class AutopilotCoordinator {
|
|
|
1283
1301
|
l1.reviewStatus === 'REVIEWED' &&
|
|
1284
1302
|
(!reportedL1.has(l1.id) || epochNeedsReport(checkpointTarget('l1', l1.id)))));
|
|
1285
1303
|
}
|
|
1286
|
-
currentExecutor(sessionId, plan, continuationCount, persisted) {
|
|
1304
|
+
currentExecutor(sessionId, plan, continuationCount, persisted, activity = this.deps.activity(sessionId)) {
|
|
1287
1305
|
const stepIndex = plan.steps.findIndex((step) => step.id === plan.currentStepId || step.state === 'WIP');
|
|
1288
1306
|
const index = stepIndex >= 0 ? stepIndex : plan.steps.findIndex((step) => step.state !== 'DONE');
|
|
1289
1307
|
if (index < 0)
|
|
1290
1308
|
return undefined;
|
|
1291
1309
|
const step = plan.steps[index];
|
|
1292
1310
|
const canonicalPosition = `L${index + 1}`;
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
(
|
|
1311
|
+
const candidates = activity?.subagents.filter((candidate) => candidate.canonicalPosition === canonicalPosition) ?? [];
|
|
1312
|
+
const assigned = persisted?.assignment?.canonicalPosition === canonicalPosition
|
|
1313
|
+
? persisted.assignment
|
|
1314
|
+
: undefined;
|
|
1315
|
+
const replacementChild = persisted?.replacement
|
|
1316
|
+
? candidates.find((candidate) => candidate.canonicalTaskName === persisted.replacement.canonicalTaskName &&
|
|
1317
|
+
(candidate.continuationGeneration ?? 1) === persisted.replacement.generation &&
|
|
1318
|
+
candidate.taskPath?.split('/').filter(Boolean).at(-1) ===
|
|
1319
|
+
persisted.replacement.taskName)
|
|
1320
|
+
: undefined;
|
|
1321
|
+
const child = replacementChild ??
|
|
1322
|
+
(assigned
|
|
1323
|
+
? candidates.find((candidate) => candidate.canonicalTaskName === assigned.canonicalTaskName &&
|
|
1324
|
+
candidate.taskPath === assigned.taskPath &&
|
|
1325
|
+
(candidate.threadId ?? candidate.id) === assigned.threadId &&
|
|
1326
|
+
(candidate.continuationGeneration ?? 1) === assigned.generation)
|
|
1327
|
+
: candidates
|
|
1328
|
+
.filter((candidate) => candidate.canonicalPosition === canonicalPosition)
|
|
1329
|
+
.sort((left, right) => (right.continuationGeneration ?? 1) - (left.continuationGeneration ?? 1) ||
|
|
1330
|
+
Date.parse(right.lastActivityAt) - Date.parse(left.lastActivityAt) ||
|
|
1331
|
+
(left.threadId ?? left.id).localeCompare(right.threadId ?? right.id))[0]);
|
|
1299
1332
|
if (!child) {
|
|
1300
|
-
if (persisted?.canonicalPosition !== canonicalPosition ||
|
|
1301
|
-
persisted.outcome === 'cancelled' ||
|
|
1302
|
-
persisted.outcome === 'failed')
|
|
1333
|
+
if (persisted?.canonicalPosition !== canonicalPosition || persisted.outcome === 'cancelled')
|
|
1303
1334
|
return undefined;
|
|
1304
1335
|
return {
|
|
1305
1336
|
...persisted,
|
|
@@ -1308,16 +1339,20 @@ export class AutopilotCoordinator {
|
|
|
1308
1339
|
ownedProcesses: refreshPersistedProcesses(persisted.ownedProcesses, [], this.deps.now()),
|
|
1309
1340
|
};
|
|
1310
1341
|
}
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1342
|
+
// Once adopted, a persisted assignment outranks every unrelated roster
|
|
1343
|
+
// entry. A new physical writer can enter only through the exact durable
|
|
1344
|
+
// replacement authorization above.
|
|
1314
1345
|
if (!child.taskPath || !child.canonicalTaskName)
|
|
1315
1346
|
return undefined;
|
|
1316
|
-
if (child.outcome === 'cancelled'
|
|
1347
|
+
if (child.outcome === 'cancelled')
|
|
1317
1348
|
return undefined;
|
|
1318
1349
|
const activeL2 = step.children.find((candidate) => candidate.state === 'WIP');
|
|
1350
|
+
const active = child.state === 'working' ||
|
|
1351
|
+
child.state === 'awaitingAgent' ||
|
|
1352
|
+
Boolean(child.ownedProcesses?.some((process) => process.state === 'running' || process.state === 'detached-active'));
|
|
1319
1353
|
const outcome = classifyExecutorOutcome({
|
|
1320
1354
|
objectiveComplete: step.state === 'DONE',
|
|
1355
|
+
active,
|
|
1321
1356
|
reportedOutcome: child.outcome,
|
|
1322
1357
|
});
|
|
1323
1358
|
return {
|
|
@@ -1346,6 +1381,15 @@ export class AutopilotCoordinator {
|
|
|
1346
1381
|
? { resumeFailures: persisted.resumeFailures }
|
|
1347
1382
|
: {}),
|
|
1348
1383
|
...(persisted?.commands ? { commands: persisted.commands } : {}),
|
|
1384
|
+
assignment: replacementChild || !assigned
|
|
1385
|
+
? executorAssignment({
|
|
1386
|
+
canonicalPosition,
|
|
1387
|
+
canonicalTaskName: child.canonicalTaskName,
|
|
1388
|
+
taskPath: child.taskPath,
|
|
1389
|
+
threadId: child.threadId ?? child.id,
|
|
1390
|
+
continuationGeneration: child.continuationGeneration ?? 1,
|
|
1391
|
+
})
|
|
1392
|
+
: assigned,
|
|
1349
1393
|
};
|
|
1350
1394
|
}
|
|
1351
1395
|
/** A fresh roster may expose two physical writers for one canonical L1. */
|
|
@@ -1661,7 +1705,8 @@ export class AutopilotCoordinator {
|
|
|
1661
1705
|
return conditions.some((condition) => this.semanticEvent(sessionId, condition));
|
|
1662
1706
|
}
|
|
1663
1707
|
freshExecutorIdentity(sessionId) {
|
|
1664
|
-
const
|
|
1708
|
+
const state = this.deps.store.find(sessionId);
|
|
1709
|
+
const replacement = state?.executor?.replacement;
|
|
1665
1710
|
if (replacement && this.validReplacement(sessionId, replacement))
|
|
1666
1711
|
return replacement;
|
|
1667
1712
|
const plan = this.deps.plan(sessionId)?.plan;
|
|
@@ -1672,11 +1717,22 @@ export class AutopilotCoordinator {
|
|
|
1672
1717
|
if (index < 0)
|
|
1673
1718
|
return undefined;
|
|
1674
1719
|
const canonicalTaskName = `l${index + 1}`;
|
|
1675
|
-
|
|
1720
|
+
// A root boundary starts a root turn, not a replacement executor. The
|
|
1721
|
+
// existing physical owner remains resumable through ordinary L2, review,
|
|
1722
|
+
// and completion boundaries, regardless of its most recently observed
|
|
1723
|
+
// continuation generation. Only a durable replacement handoff above may
|
|
1724
|
+
// authorize a generated physical task name.
|
|
1725
|
+
if (state?.executor?.canonicalTaskName === canonicalTaskName &&
|
|
1726
|
+
state.executor.outcome !== 'cancelled')
|
|
1727
|
+
return undefined;
|
|
1728
|
+
// Legacy roster observations establish a resumable owner but never
|
|
1729
|
+
// authorize a generated name. Only scheduleExecutorReplacement creates
|
|
1730
|
+
// the exact durable replacement record consumed above.
|
|
1731
|
+
if (this.deps
|
|
1676
1732
|
.activity(sessionId)
|
|
1677
|
-
?.subagents.
|
|
1678
|
-
|
|
1679
|
-
return executorIdentity(canonicalTaskName,
|
|
1733
|
+
?.subagents.some((child) => child.canonicalTaskName === canonicalTaskName))
|
|
1734
|
+
return undefined;
|
|
1735
|
+
return executorIdentity(canonicalTaskName, 1);
|
|
1680
1736
|
}
|
|
1681
1737
|
validReplacement(sessionId, replacement) {
|
|
1682
1738
|
const retained = this.deps.plan(sessionId);
|
|
@@ -1693,16 +1749,24 @@ export class AutopilotCoordinator {
|
|
|
1693
1749
|
const retained = this.deps.plan(sessionId);
|
|
1694
1750
|
if (!current?.requestedEnabled || !retained || current.executor?.replacement)
|
|
1695
1751
|
return;
|
|
1696
|
-
const identity = executorIdentity(executor.canonicalTaskName, executor.continuationGeneration + 1);
|
|
1752
|
+
const identity = executorIdentity(executor.canonicalTaskName, (executor.assignment?.generation ?? executor.continuationGeneration) + 1);
|
|
1697
1753
|
const now = this.deps.now();
|
|
1698
1754
|
const controlId = this.deps.nextControlId(sessionId, current.generation);
|
|
1699
1755
|
const replacement = {
|
|
1700
1756
|
...executor,
|
|
1701
1757
|
outcome: 'failed',
|
|
1758
|
+
...(executor.assignment
|
|
1759
|
+
? { assignment: { ...executor.assignment, state: 'unavailable' } }
|
|
1760
|
+
: {}),
|
|
1702
1761
|
replacement: {
|
|
1703
1762
|
...identity,
|
|
1704
1763
|
planIdentity: retained.identity,
|
|
1705
1764
|
planFingerprint: fingerprint(retained.plan),
|
|
1765
|
+
previous: executor.assignment
|
|
1766
|
+
? { ...executor.assignment, state: 'unavailable' }
|
|
1767
|
+
: executorAssignment(executor, 'unavailable'),
|
|
1768
|
+
reason: 'explicitExecutorRejection',
|
|
1769
|
+
evidence: 'retry-limit-exhausted',
|
|
1706
1770
|
},
|
|
1707
1771
|
commands: executor.commands?.map((command) => ['scheduled', 'issued'].includes(command.status)
|
|
1708
1772
|
? { ...command, status: 'superseded', updatedAt: now }
|
|
@@ -13,6 +13,19 @@ export const blockingResumeConditions = {
|
|
|
13
13
|
externalState: 'externalStateChanged',
|
|
14
14
|
materialAmbiguity: 'userGuidance',
|
|
15
15
|
};
|
|
16
|
+
export function executorAssignment(executor, state = 'resumable') {
|
|
17
|
+
const taskName = executor.taskPath.split('/').filter(Boolean).at(-1);
|
|
18
|
+
const identity = executorIdentity(executor.canonicalTaskName, executor.continuationGeneration);
|
|
19
|
+
return {
|
|
20
|
+
canonicalPosition: executor.canonicalPosition,
|
|
21
|
+
canonicalTaskName: executor.canonicalTaskName,
|
|
22
|
+
taskName: taskName || identity.taskName,
|
|
23
|
+
taskPath: executor.taskPath,
|
|
24
|
+
threadId: executor.threadId,
|
|
25
|
+
generation: identity.generation,
|
|
26
|
+
state,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
16
29
|
export function checkpointTarget(kind, l1Id, l2Id) {
|
|
17
30
|
return kind === 'l2'
|
|
18
31
|
? JSON.stringify([kind, l1Id, l2Id])
|
|
@@ -153,6 +166,8 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
153
166
|
const resumeFailures = executorValue.resumeFailures === undefined
|
|
154
167
|
? 0
|
|
155
168
|
: nonNegativeInteger(executorValue.resumeFailures);
|
|
169
|
+
const assignmentValue = executorValue.assignment === undefined ? undefined : record(executorValue.assignment);
|
|
170
|
+
const assignment = assignmentValue ? parseExecutorAssignment(assignmentValue) : undefined;
|
|
156
171
|
const replacementValue = executorValue.replacement === undefined ? undefined : record(executorValue.replacement);
|
|
157
172
|
const replacementGeneration = replacementValue && nonNegativeInteger(replacementValue.generation);
|
|
158
173
|
const replacementTaskName = replacementValue && boundedText(replacementValue.taskName);
|
|
@@ -160,6 +175,12 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
160
175
|
const replacementCanonicalPosition = replacementValue && boundedText(replacementValue.canonicalPosition);
|
|
161
176
|
const replacementPlanIdentity = replacementValue && boundedText(replacementValue.planIdentity);
|
|
162
177
|
const replacementPlanFingerprint = replacementValue && boundedText(replacementValue.planFingerprint);
|
|
178
|
+
const replacementPreviousValue = replacementValue && record(replacementValue.previous);
|
|
179
|
+
const replacementPrevious = replacementPreviousValue
|
|
180
|
+
? parseExecutorAssignment(replacementPreviousValue)
|
|
181
|
+
: undefined;
|
|
182
|
+
const replacementReason = replacementValue && stringValue(replacementValue.reason, ['explicitExecutorRejection']);
|
|
183
|
+
const replacementEvidence = replacementValue && boundedText(replacementValue.evidence);
|
|
163
184
|
if (!canonicalPosition ||
|
|
164
185
|
!canonicalTaskName ||
|
|
165
186
|
!taskPath ||
|
|
@@ -170,14 +191,32 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
170
191
|
continuationGeneration === null ||
|
|
171
192
|
continuationCount === null ||
|
|
172
193
|
resumeFailures === null ||
|
|
194
|
+
(executorValue.assignment !== undefined && !assignment) ||
|
|
173
195
|
(replacementValue &&
|
|
174
196
|
(!replacementGeneration ||
|
|
175
197
|
!replacementTaskName ||
|
|
176
198
|
!replacementCanonicalTaskName ||
|
|
177
199
|
!replacementCanonicalPosition ||
|
|
178
200
|
!replacementPlanIdentity ||
|
|
179
|
-
!replacementPlanFingerprint
|
|
201
|
+
!replacementPlanFingerprint ||
|
|
202
|
+
(replacementValue.previous !== undefined && !replacementPrevious) ||
|
|
203
|
+
(replacementReason !== undefined && replacementReason !== 'explicitExecutorRejection') ||
|
|
204
|
+
(replacementValue.evidence !== undefined && !replacementEvidence))))
|
|
180
205
|
return undefined;
|
|
206
|
+
if (replacementValue) {
|
|
207
|
+
try {
|
|
208
|
+
const identity = executorIdentity(replacementCanonicalTaskName, replacementGeneration);
|
|
209
|
+
if (identity.taskName !== replacementTaskName ||
|
|
210
|
+
identity.canonicalPosition !== replacementCanonicalPosition ||
|
|
211
|
+
(replacementPrevious &&
|
|
212
|
+
(replacementPrevious.canonicalTaskName !== canonicalTaskName ||
|
|
213
|
+
replacementPrevious.canonicalPosition !== canonicalPosition)))
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
181
220
|
const commandsValue = executorValue.commands;
|
|
182
221
|
if (commandsValue !== undefined && (!Array.isArray(commandsValue) || commandsValue.length > 32))
|
|
183
222
|
return undefined;
|
|
@@ -268,6 +307,7 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
268
307
|
continuationGeneration,
|
|
269
308
|
continuationCount,
|
|
270
309
|
...(resumeFailures ? { resumeFailures } : {}),
|
|
310
|
+
...(assignment ? { assignment } : {}),
|
|
271
311
|
...(replacementValue
|
|
272
312
|
? {
|
|
273
313
|
replacement: {
|
|
@@ -277,6 +317,9 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
277
317
|
canonicalPosition: replacementCanonicalPosition,
|
|
278
318
|
planIdentity: replacementPlanIdentity,
|
|
279
319
|
planFingerprint: replacementPlanFingerprint,
|
|
320
|
+
...(replacementPrevious ? { previous: replacementPrevious } : {}),
|
|
321
|
+
...(replacementReason ? { reason: replacementReason } : {}),
|
|
322
|
+
...(replacementEvidence ? { evidence: replacementEvidence } : {}),
|
|
280
323
|
},
|
|
281
324
|
}
|
|
282
325
|
: {}),
|
|
@@ -396,19 +439,50 @@ function parseCheckpoints(value) {
|
|
|
396
439
|
terminalReviewAccepted: value.terminalReviewAccepted,
|
|
397
440
|
};
|
|
398
441
|
}
|
|
442
|
+
function parseExecutorAssignment(value) {
|
|
443
|
+
const canonicalPosition = boundedText(value.canonicalPosition);
|
|
444
|
+
const canonicalTaskName = boundedText(value.canonicalTaskName);
|
|
445
|
+
const taskName = boundedText(value.taskName);
|
|
446
|
+
const taskPath = boundedText(value.taskPath);
|
|
447
|
+
const threadId = boundedText(value.threadId);
|
|
448
|
+
const generation = nonNegativeInteger(value.generation);
|
|
449
|
+
const state = stringValue(value.state, ['resumable', 'closed', 'unavailable']);
|
|
450
|
+
if (!canonicalPosition ||
|
|
451
|
+
!canonicalTaskName ||
|
|
452
|
+
!taskName ||
|
|
453
|
+
!taskPath ||
|
|
454
|
+
!threadId ||
|
|
455
|
+
generation === null ||
|
|
456
|
+
generation < 1 ||
|
|
457
|
+
!state)
|
|
458
|
+
return undefined;
|
|
459
|
+
try {
|
|
460
|
+
const identity = executorIdentity(canonicalTaskName, generation);
|
|
461
|
+
return identity.canonicalPosition === canonicalPosition && identity.taskName === taskName
|
|
462
|
+
? { canonicalPosition, canonicalTaskName, taskName, taskPath, threadId, generation, state }
|
|
463
|
+
: undefined;
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
399
469
|
export function classifyExecutorOutcome(input) {
|
|
400
470
|
if (input.objectiveComplete)
|
|
401
|
-
return { outcome: 'objective_complete' };
|
|
402
|
-
if (input.reportedOutcome === 'cancelled' || input.reportedOutcome === 'failed')
|
|
403
|
-
return { outcome: input.reportedOutcome };
|
|
471
|
+
return { classification: 'done', outcome: 'objective_complete' };
|
|
404
472
|
const blocking = input.blockingReason && input.resumeCondition
|
|
405
473
|
? { reason: input.blockingReason, resumeCondition: input.resumeCondition }
|
|
406
474
|
: undefined;
|
|
407
475
|
if (input.reportedOutcome === 'blocked' && validStructuredBlock(blocking))
|
|
408
|
-
return { outcome: 'blocked', blocking };
|
|
476
|
+
return { classification: 'blocker', outcome: 'blocked', blocking };
|
|
477
|
+
if (input.active)
|
|
478
|
+
// Fresh activity outranks the previous turn's terminal transport status.
|
|
479
|
+
return { classification: 'ongoing', outcome: 'partial' };
|
|
480
|
+
if (input.reportedOutcome === 'cancelled' || input.reportedOutcome === 'failed')
|
|
481
|
+
return { classification: 'recoverableFailure', outcome: input.reportedOutcome };
|
|
409
482
|
// A turn ending, a checkpoint, or free-form language about time/context is
|
|
410
|
-
// not objective state. Incomplete Org state
|
|
411
|
-
return
|
|
483
|
+
// not objective state. Incomplete, settled Org state is mechanically
|
|
484
|
+
// recoverable and must return to the same canonical executor.
|
|
485
|
+
return { classification: 'recoverableFailure', outcome: 'partial' };
|
|
412
486
|
}
|
|
413
487
|
export function decideSupervisedLifecycle(input) {
|
|
414
488
|
if (input.explicitlyPausedOrCancelled || executionComplete(input.plan))
|
|
@@ -176,13 +176,33 @@ export function parseOrgPlanCheckpoint(value) {
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
/** Acknowledgement deliberately contains no plan path, findings, or model text. */
|
|
179
|
-
export function toOrgPlanCheckpointToolResponse() {
|
|
179
|
+
export function toOrgPlanCheckpointToolResponse(status) {
|
|
180
|
+
const result = status === 'recorded'
|
|
181
|
+
? {
|
|
182
|
+
status,
|
|
183
|
+
durable: true,
|
|
184
|
+
next: 'emitBoundaryFinalAndEndTurn',
|
|
185
|
+
allowFurtherTools: false,
|
|
186
|
+
}
|
|
187
|
+
: status === 'alreadyRecorded'
|
|
188
|
+
? {
|
|
189
|
+
status,
|
|
190
|
+
durable: true,
|
|
191
|
+
next: 'continueFromDurableCheckpoint',
|
|
192
|
+
allowBoundaryFinal: false,
|
|
193
|
+
}
|
|
194
|
+
: {
|
|
195
|
+
status,
|
|
196
|
+
durable: false,
|
|
197
|
+
next: 'reconcileCheckpointState',
|
|
198
|
+
allowBoundaryFinal: false,
|
|
199
|
+
};
|
|
180
200
|
return {
|
|
181
|
-
success:
|
|
201
|
+
success: status !== 'failed',
|
|
182
202
|
contentItems: [
|
|
183
203
|
{
|
|
184
204
|
type: 'inputText',
|
|
185
|
-
text:
|
|
205
|
+
text: JSON.stringify(result),
|
|
186
206
|
},
|
|
187
207
|
],
|
|
188
208
|
};
|