gestalt-mobile 0.41.0 → 0.41.2
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-9uZwieND.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/server/composition.contracts.js +200 -44
- package/dist/server/server/composition.js +155 -37
- package/dist/server/server/features/agent-activity/model.js +8 -2
- 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/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/platform/persistence/sqlite-pending-interaction-store.js +12 -6
- package/dist/server/shared/contracts/org-plan-attention.js +8 -1
- package/dist/server/shared/contracts/org-plan-checkpoint.js +27 -3
- package/dist/server/shared/contracts/plan-signal.js +3 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { debuglog } from 'node:util';
|
|
10
11
|
import { buildApp } from './app.js';
|
|
11
12
|
import { FilesystemWorkspaceCatalog } from './platform/catalog/filesystem-workspace-catalog.js';
|
|
12
13
|
import { FilesystemWorkspaceFiles } from './platform/filesystem/filesystem-workspace-files.js';
|
|
@@ -48,7 +49,7 @@ import { toAgentActivityDto } from './features/agent-activity/activity-dto.js';
|
|
|
48
49
|
import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
|
|
49
50
|
import { completedCommandId, compactOrgPlanCheckpointKind, isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
|
|
50
51
|
import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
|
|
51
|
-
import { resolveOrgPlanCheckpointSignal,
|
|
52
|
+
import { hasActiveL1Writer, resolveOrgPlanCheckpointSignal, validateOrgPlanCheckpoint, } from './features/org-plan-checkpoint/application/validate.js';
|
|
52
53
|
import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
|
|
53
54
|
import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
|
|
54
55
|
import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
|
|
@@ -65,11 +66,13 @@ import { AutopilotCoordinator } from './features/autopilot/application/service.j
|
|
|
65
66
|
import { deriveSessionStatus } from './features/sessions/session-status.js';
|
|
66
67
|
import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, autopilotExecutorLaunchPrompt, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
|
|
67
68
|
import { createRelyingPartyConfig } from './config.js';
|
|
68
|
-
import { parseOrgPlanAttention } from '../shared/contracts/org-plan-attention.js';
|
|
69
|
+
import { parseOrgPlanAttention, toOrgPlanAttentionAcknowledgement, } from '../shared/contracts/org-plan-attention.js';
|
|
69
70
|
import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
|
|
70
71
|
import { agentCapacityRecoveryToolResponse } from '../shared/contracts/agent-capacity-recovery.js';
|
|
71
72
|
import { pwaIconUrl } from './features/pwa/register-routes.js';
|
|
72
73
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
74
|
+
const checkpointDiagnostic = debuglog('gestalt-mobile:checkpoint');
|
|
75
|
+
const attentionDiagnostic = debuglog('gestalt-mobile:attention');
|
|
73
76
|
export async function composeRelayApp(options) {
|
|
74
77
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
75
78
|
const relyingParty = createRelyingPartyConfig(options.relyingParty.publicOrigin);
|
|
@@ -104,6 +107,7 @@ export async function composeRelayApp(options) {
|
|
|
104
107
|
const autopilotStore = new SqliteAutopilotStore(database);
|
|
105
108
|
const attentionResolutionOperations = new Map();
|
|
106
109
|
const checkpointHandoffTimers = new Map();
|
|
110
|
+
const attentionAcknowledgementTimers = new Map();
|
|
107
111
|
const checkpointHandoffRecoveries = new Set();
|
|
108
112
|
const supervisedPlans = new SupervisedPlanRegistry();
|
|
109
113
|
const planStatusSource = new FilesystemPlanStatusSource(join(dirname(databasePath), 'plans'));
|
|
@@ -471,6 +475,9 @@ export async function composeRelayApp(options) {
|
|
|
471
475
|
// request: the client can deliver them as a follow-up prompt instead.
|
|
472
476
|
if (interaction.kind === 'quiz')
|
|
473
477
|
continue;
|
|
478
|
+
if (interaction.kind === 'orgPlanAttention' &&
|
|
479
|
+
interactions.resolutionState(sessionId, interaction.requestId) === 'acknowledged')
|
|
480
|
+
continue;
|
|
474
481
|
if (interactions.resolve(sessionId, interaction.requestId, occurredAt, outcome))
|
|
475
482
|
publishInteractionResolved(sessionId, interaction.requestId, occurredAt, outcome);
|
|
476
483
|
}
|
|
@@ -551,6 +558,36 @@ export async function composeRelayApp(options) {
|
|
|
551
558
|
handle.unref?.();
|
|
552
559
|
checkpointHandoffTimers.set(key, { handle, token });
|
|
553
560
|
};
|
|
561
|
+
const cancelAttentionAcknowledgementTimer = (sessionId, requestId) => {
|
|
562
|
+
const key = checkpointTimerKey(sessionId, requestId);
|
|
563
|
+
const timer = attentionAcknowledgementTimers.get(key);
|
|
564
|
+
if (!timer)
|
|
565
|
+
return;
|
|
566
|
+
clearTimeout(timer.handle);
|
|
567
|
+
attentionAcknowledgementTimers.delete(key);
|
|
568
|
+
};
|
|
569
|
+
const armAttentionAcknowledgementDeadline = (sessionId, requestId) => {
|
|
570
|
+
cancelAttentionAcknowledgementTimer(sessionId, requestId);
|
|
571
|
+
const key = checkpointTimerKey(sessionId, requestId);
|
|
572
|
+
const token = Symbol(key);
|
|
573
|
+
const handle = setTimeout(() => {
|
|
574
|
+
if (attentionAcknowledgementTimers.get(key)?.token !== token)
|
|
575
|
+
return;
|
|
576
|
+
attentionAcknowledgementTimers.delete(key);
|
|
577
|
+
const ownedRuntime = runtime;
|
|
578
|
+
const session = sessions.find(sessionId);
|
|
579
|
+
if (!ownedRuntime || !session || !ownedRuntime.abandonServerRequest(sessionId, requestId))
|
|
580
|
+
return;
|
|
581
|
+
void ownedRuntime
|
|
582
|
+
.recycle(session, new Date().toISOString())
|
|
583
|
+
.then((recovered) => saveSession(recovered))
|
|
584
|
+
.catch(() => {
|
|
585
|
+
// The durable attention survives; normal session recovery can reacquire the writer.
|
|
586
|
+
});
|
|
587
|
+
}, options.attentionAcknowledgementDeadlineMs ?? 5_000);
|
|
588
|
+
handle.unref?.();
|
|
589
|
+
attentionAcknowledgementTimers.set(key, { handle, token });
|
|
590
|
+
};
|
|
554
591
|
const failPendingCheckpointHandoffs = (sessionId, recoverBoundary) => {
|
|
555
592
|
for (const interaction of interactions.list(sessionId)) {
|
|
556
593
|
if (interaction.kind !== 'orgPlanCheckpoint')
|
|
@@ -568,28 +605,38 @@ export async function composeRelayApp(options) {
|
|
|
568
605
|
* enters the durable store, requests human attention, and notifies autopilot.
|
|
569
606
|
* Both the codex JSON-RPC handler and the kimi polling handler land here.
|
|
570
607
|
*/
|
|
571
|
-
const acceptPendingInteraction = (sessionId, rawInteraction, session) => {
|
|
608
|
+
const acceptPendingInteraction = (sessionId, rawInteraction, session, acknowledgeAttention = false) => {
|
|
572
609
|
const interaction = {
|
|
573
610
|
...rawInteraction,
|
|
574
611
|
turnId: session.activeTurnId,
|
|
575
612
|
requestedAt: new Date().toISOString(),
|
|
576
613
|
};
|
|
577
|
-
interactions.add(sessionId, interaction);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
614
|
+
interactions.add(sessionId, interaction, acknowledgeAttention && interaction.kind === 'orgPlanAttention' ? 'acknowledged' : 'active');
|
|
615
|
+
try {
|
|
616
|
+
activity.observe({
|
|
617
|
+
sessionId,
|
|
618
|
+
occurredAt: interaction.requestedAt,
|
|
619
|
+
kind: 'interactionPending',
|
|
620
|
+
...(interaction.kind === 'orgPlanAttention'
|
|
621
|
+
? { attentionReason: interaction.payload.reason }
|
|
622
|
+
: {}),
|
|
623
|
+
});
|
|
624
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
625
|
+
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
626
|
+
sessions.save(updated);
|
|
627
|
+
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
628
|
+
if (interaction.kind === 'orgPlanAttention')
|
|
629
|
+
events.publish(journal.append(sessionId, 'org-plan.attention-required', interaction, updated.updatedAt));
|
|
630
|
+
autopilot.evaluate(sessionId);
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (!acknowledgeAttention || interaction.kind !== 'orgPlanAttention')
|
|
634
|
+
throw error;
|
|
635
|
+
attentionDiagnostic('%o', {
|
|
636
|
+
requestId: interaction.requestId,
|
|
637
|
+
reasonCode: 'postPersistenceProjectionFailed',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
593
640
|
return true;
|
|
594
641
|
};
|
|
595
642
|
const acceptPlanUpdate = (sessionId, update) => {
|
|
@@ -625,7 +672,9 @@ export async function composeRelayApp(options) {
|
|
|
625
672
|
if (resolvedRequestId) {
|
|
626
673
|
const interaction = interactions.find(sessionId, resolvedRequestId);
|
|
627
674
|
const outcome = interaction?.kind === 'orgPlanAttention' ? 'failed' : 'dismissed';
|
|
628
|
-
if (interaction?.kind
|
|
675
|
+
if (!(interaction?.kind === 'orgPlanAttention' &&
|
|
676
|
+
interactions.resolutionState(sessionId, resolvedRequestId) === 'acknowledged') &&
|
|
677
|
+
interaction?.kind !== 'quiz' &&
|
|
629
678
|
interactions.resolve(sessionId, resolvedRequestId, occurredAt, outcome)) {
|
|
630
679
|
if (outcome === 'failed')
|
|
631
680
|
publishAttentionSettlement(sessionId, resolvedRequestId, occurredAt, outcome);
|
|
@@ -683,7 +732,7 @@ export async function composeRelayApp(options) {
|
|
|
683
732
|
const retained = supervisedPlans.find(sessionId);
|
|
684
733
|
const identity = supervisedPlans.identity(sessionId);
|
|
685
734
|
const checkpoint = retained && identity
|
|
686
|
-
? resolveOrgPlanCheckpointSignal(compactCheckpointKind, retained, identity)
|
|
735
|
+
? resolveOrgPlanCheckpointSignal(compactCheckpointKind, retained, identity, supervisedPlans.publicationReason(sessionId))
|
|
687
736
|
: null;
|
|
688
737
|
if (checkpoint)
|
|
689
738
|
rawInteraction = {
|
|
@@ -741,25 +790,38 @@ export async function composeRelayApp(options) {
|
|
|
741
790
|
!identity ||
|
|
742
791
|
checkpoint.planIdentity !== identity ||
|
|
743
792
|
!rootOwned)
|
|
744
|
-
return
|
|
745
|
-
|
|
793
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, checkpointFailureResponse(!checkpoint
|
|
794
|
+
? compactCheckpointKind === 'l1Accepted'
|
|
795
|
+
? 'acceptedL1NotResolved'
|
|
796
|
+
: 'checkpointPayloadInvalid'
|
|
797
|
+
: !retained
|
|
798
|
+
? 'planProjectionUnavailable'
|
|
799
|
+
: !identity || checkpoint.planIdentity !== identity
|
|
800
|
+
? 'planIdentityMismatch'
|
|
801
|
+
: 'rootNotOwner')) === true);
|
|
802
|
+
const validation = validateOrgPlanCheckpoint({
|
|
746
803
|
checkpoint,
|
|
747
804
|
plan: retained,
|
|
748
805
|
planIdentity: identity,
|
|
749
806
|
rootOwned,
|
|
750
807
|
hasActiveL1Writer: (position) => hasActiveL1Writer(activity.snapshot(sessionId, new Date().toISOString()), position),
|
|
751
808
|
});
|
|
752
|
-
if (!valid)
|
|
753
|
-
return
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
809
|
+
if (!validation.valid || !session.activeTurnId)
|
|
810
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, checkpointFailureResponse(validation.valid ? 'rootTurnNotActive' : validation.reasonCode, {
|
|
811
|
+
...(!validation.valid && validation.expected
|
|
812
|
+
? { expected: validation.expected }
|
|
813
|
+
: {}),
|
|
814
|
+
...(!validation.valid && validation.observed
|
|
815
|
+
? { observed: validation.observed }
|
|
816
|
+
: {}),
|
|
817
|
+
})) === true);
|
|
818
|
+
let recordStatus = 'failed';
|
|
757
819
|
try {
|
|
758
|
-
|
|
820
|
+
recordStatus = autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString());
|
|
759
821
|
}
|
|
760
822
|
catch {
|
|
761
823
|
if (!autopilot.checkpointHandoffFailed(sessionId, session.activeTurnId))
|
|
762
|
-
return
|
|
824
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, checkpointFailureResponse('checkpointPersistenceFailed')) === true);
|
|
763
825
|
const interaction = {
|
|
764
826
|
...rawInteraction,
|
|
765
827
|
turnId: session.activeTurnId,
|
|
@@ -769,8 +831,10 @@ export async function composeRelayApp(options) {
|
|
|
769
831
|
failCheckpointHandoff(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
770
832
|
return true;
|
|
771
833
|
}
|
|
772
|
-
if (
|
|
773
|
-
return
|
|
834
|
+
if (recordStatus === 'failed')
|
|
835
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, checkpointFailureResponse('checkpointStateConflict')) === true);
|
|
836
|
+
if (recordStatus === 'alreadyRecorded')
|
|
837
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('alreadyRecorded')) === true);
|
|
774
838
|
const interaction = {
|
|
775
839
|
...rawInteraction,
|
|
776
840
|
turnId: session.activeTurnId,
|
|
@@ -779,7 +843,7 @@ export async function composeRelayApp(options) {
|
|
|
779
843
|
interactions.add(sessionId, interaction);
|
|
780
844
|
armCheckpointHandoffDeadline(sessionId, rawInteraction.requestId, session.activeTurnId);
|
|
781
845
|
try {
|
|
782
|
-
runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse());
|
|
846
|
+
runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse('recorded'));
|
|
783
847
|
}
|
|
784
848
|
catch {
|
|
785
849
|
// The accepted hold remains owned by the bounded deadline.
|
|
@@ -788,6 +852,24 @@ export async function composeRelayApp(options) {
|
|
|
788
852
|
// is cancelled solely by the JSON-RPC write-settlement callback.
|
|
789
853
|
return true;
|
|
790
854
|
}
|
|
855
|
+
if (rawInteraction.kind === 'orgPlanAttention') {
|
|
856
|
+
try {
|
|
857
|
+
acceptPendingInteraction(sessionId, rawInteraction, session, true);
|
|
858
|
+
const acknowledged = runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanAttentionAcknowledgement()) === true;
|
|
859
|
+
if (acknowledged)
|
|
860
|
+
armAttentionAcknowledgementDeadline(sessionId, rawInteraction.requestId);
|
|
861
|
+
return acknowledged;
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
const rejected = runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanAttentionAcknowledgement({
|
|
865
|
+
accepted: false,
|
|
866
|
+
reason: 'persistenceFailed',
|
|
867
|
+
})) === true;
|
|
868
|
+
if (rejected)
|
|
869
|
+
armAttentionAcknowledgementDeadline(sessionId, rawInteraction.requestId);
|
|
870
|
+
return rejected;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
791
873
|
return acceptPendingInteraction(sessionId, rawInteraction, session);
|
|
792
874
|
}, (sessionId) => {
|
|
793
875
|
activity.disconnected(sessionId, new Date().toISOString());
|
|
@@ -827,7 +909,18 @@ export async function composeRelayApp(options) {
|
|
|
827
909
|
const handleKimiServerRequest = (sessionId, request) => {
|
|
828
910
|
const rawInteraction = toPendingInteraction(request);
|
|
829
911
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
830
|
-
|
|
912
|
+
if (!rawInteraction || !session)
|
|
913
|
+
return false;
|
|
914
|
+
if (rawInteraction.kind !== 'orgPlanAttention')
|
|
915
|
+
return acceptPendingInteraction(sessionId, rawInteraction, session);
|
|
916
|
+
try {
|
|
917
|
+
acceptPendingInteraction(sessionId, rawInteraction, session, true);
|
|
918
|
+
void kimiRuntime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanAttentionAcknowledgement());
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
void kimiRuntime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanAttentionAcknowledgement({ accepted: false, reason: 'persistenceFailed' }));
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
831
924
|
};
|
|
832
925
|
kimiRuntime = kimiManager
|
|
833
926
|
? new KimiSessionRuntime({
|
|
@@ -850,6 +943,10 @@ export async function composeRelayApp(options) {
|
|
|
850
943
|
})
|
|
851
944
|
: null;
|
|
852
945
|
runtime?.onServerResponseSettled((sessionId, requestId, outcome) => {
|
|
946
|
+
if (attentionAcknowledgementTimers.has(checkpointTimerKey(sessionId, requestId))) {
|
|
947
|
+
cancelAttentionAcknowledgementTimer(sessionId, requestId);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
853
950
|
const interaction = interactions.find(sessionId, requestId);
|
|
854
951
|
if (interaction?.kind !== 'orgPlanCheckpoint')
|
|
855
952
|
return;
|
|
@@ -1347,6 +1444,18 @@ export async function composeRelayApp(options) {
|
|
|
1347
1444
|
return { kind: 'staleOperation' };
|
|
1348
1445
|
if (claim === 'missing')
|
|
1349
1446
|
return { kind: 'noActive' };
|
|
1447
|
+
const acknowledged = interactions.resolutionState(sessionId, requestId) === 'acknowledged';
|
|
1448
|
+
if (acknowledged) {
|
|
1449
|
+
if (!interactions.beginDelivery(sessionId, requestId, operationKey))
|
|
1450
|
+
return { kind: 'staleOperation' };
|
|
1451
|
+
const resolvedAt = new Date().toISOString();
|
|
1452
|
+
if (!interactions.settleOperation(sessionId, requestId, operationKey, resolvedAt, 'answered'))
|
|
1453
|
+
return { kind: 'staleOperation' };
|
|
1454
|
+
const accepted = { kind: 'accepted', resolvedAt };
|
|
1455
|
+
idempotency.put(scope, operationKey, 202, JSON.stringify({ kind: 'replayed', resolvedAt }));
|
|
1456
|
+
publishAttentionSettlement(sessionId, requestId, resolvedAt, 'answered');
|
|
1457
|
+
return accepted;
|
|
1458
|
+
}
|
|
1350
1459
|
// A durable capability belongs to the session/thread, not the
|
|
1351
1460
|
// relay process. A supported stopped writer is retryable;
|
|
1352
1461
|
// missing capability identifies a pre-rollout legacy thread.
|
|
@@ -1468,6 +1577,14 @@ export async function composeRelayApp(options) {
|
|
|
1468
1577
|
checkpointHandoffTimers.delete(key);
|
|
1469
1578
|
}
|
|
1470
1579
|
}
|
|
1580
|
+
for (const key of [...attentionAcknowledgementTimers.keys()]) {
|
|
1581
|
+
if (key.startsWith(`${session.id}:`)) {
|
|
1582
|
+
const timer = attentionAcknowledgementTimers.get(key);
|
|
1583
|
+
if (timer)
|
|
1584
|
+
clearTimeout(timer.handle);
|
|
1585
|
+
attentionAcknowledgementTimers.delete(key);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1471
1588
|
// Relay shutdown only releases this process's writer. A typed attention
|
|
1472
1589
|
// request remains a durable human-visible blocker for the next relay
|
|
1473
1590
|
// instance; only an app-server-cleared request is a failed audit outcome.
|
|
@@ -1497,7 +1614,8 @@ function resolveStateDatabasePath(root, stateHome) {
|
|
|
1497
1614
|
const legacyPath = legacyRelayStatePath(root, stateHome);
|
|
1498
1615
|
return existsSync(legacyPath) ? legacyPath : currentPath;
|
|
1499
1616
|
}
|
|
1500
|
-
function
|
|
1501
|
-
|
|
1502
|
-
|
|
1617
|
+
function checkpointFailureResponse(reasonCode, details = {}) {
|
|
1618
|
+
const correlationId = randomUUID();
|
|
1619
|
+
checkpointDiagnostic('%o', { correlationId, reasonCode, ...details });
|
|
1620
|
+
return toOrgPlanCheckpointToolResponse('failed', { reasonCode, correlationId, ...details });
|
|
1503
1621
|
}
|
|
@@ -201,7 +201,10 @@ function childOutcome(status, action, previous) {
|
|
|
201
201
|
function childState(status, action, previous = 'working') {
|
|
202
202
|
if (status === 'error' || status === 'failed' || status === 'systemError' || status === 'errored')
|
|
203
203
|
return 'blocked';
|
|
204
|
-
if (status === 'completed' ||
|
|
204
|
+
if (status === 'completed' ||
|
|
205
|
+
status === 'idle' ||
|
|
206
|
+
status === 'interrupted' ||
|
|
207
|
+
action === 'close_agent')
|
|
205
208
|
return 'idle';
|
|
206
209
|
// `wait` belongs to the caller/root; it does not rewrite the child which may
|
|
207
210
|
// still be working. A child can explicitly report its own waiting status.
|
|
@@ -229,7 +232,10 @@ function childReason(status, action) {
|
|
|
229
232
|
return 'processExited';
|
|
230
233
|
if (action === 'wait')
|
|
231
234
|
return 'collaborationWait';
|
|
232
|
-
if (status === 'completed' ||
|
|
235
|
+
if (status === 'completed' ||
|
|
236
|
+
status === 'idle' ||
|
|
237
|
+
status === 'interrupted' ||
|
|
238
|
+
action === 'close_agent')
|
|
233
239
|
return 'turnCompleted';
|
|
234
240
|
return 'turnActive';
|
|
235
241
|
}
|
|
@@ -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 }
|