openplanr 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/operate.d.ts +10 -1
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +55 -7
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/services/ai-service.d.ts +25 -0
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +38 -0
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +37 -1
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +351 -10
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/config.d.ts +26 -0
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +41 -0
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +121 -1
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +29 -0
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts +2 -0
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +237 -41
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/answer-service.d.ts +17 -0
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +49 -7
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +9 -3
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +13 -0
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +116 -26
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts +12 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +26 -15
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/lifecycle.d.ts +8 -0
- package/dist/services/operate/lifecycle.d.ts.map +1 -1
- package/dist/services/operate/lifecycle.js +19 -1
- package/dist/services/operate/lifecycle.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts +22 -0
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +397 -44
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +4 -1
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/reports.d.ts +9 -0
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +9 -0
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/types.d.ts +19 -0
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +25 -5
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, buildAdvisorOperatingContext, createNativeOperatingRoleResult, createOperatingAdvisorPack, } from './advisors.js';
|
|
4
|
+
import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, assertOperatingAdvisorPackWithinBudget, buildAdvisorOperatingContext, createNativeMissionOperatingRoleResult, createNativeOperatingRoleResult, createOperatingAdvisorPack, } from './advisors.js';
|
|
5
5
|
import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
|
|
6
|
-
import { operatingProjectKey } from './config.js';
|
|
7
|
-
import { buildChairEvidence } from './engine.js';
|
|
6
|
+
import { operatingProjectKey, readOperatingAdapterLeaseDurationMs, readOperatingDispatchModeOverrides, } from './config.js';
|
|
7
|
+
import { buildChairEvidence, buildOperatingMissionPackets, gateRecordedProposalCitations, } from './engine.js';
|
|
8
8
|
import { OperatingEventStore } from './event-store.js';
|
|
9
9
|
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
10
10
|
import { purgeStaleEvidenceClassifications } from './evidence-classifications.js';
|
|
@@ -13,12 +13,22 @@ import { evaluateEvidenceReadiness } from './evidence-readiness.js';
|
|
|
13
13
|
import { guidedSessionStatus, purgeGuidedSessions } from './interaction/session-service.js';
|
|
14
14
|
import { assertCommittedOperatingView, recoverOperatingTransactions } from './journal.js';
|
|
15
15
|
import { withOperatingLock } from './lock-service.js';
|
|
16
|
+
import { narrowMissionRootsToCeiling, operatingRegistryDispatchMode, operatingRuntimeEnforcesBoundedReadOnly, resolveOperatingDispatchMode, } from './mission-dispatch.js';
|
|
16
17
|
import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
|
|
17
18
|
import { containsSecret, redactSensitiveText } from './redaction.js';
|
|
18
19
|
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
19
|
-
import { resolveContainedPath, resolveOperatingPaths } from './workspace.js';
|
|
20
|
+
import { readOperatingConfig, refreshOperatingWorkspaceManifest, resolveContainedPath, resolveOperatingPaths, } from './workspace.js';
|
|
21
|
+
/**
|
|
22
|
+
* Whether any bound role of this session resolved to native mission dispatch, so
|
|
23
|
+
* both adapter-handoff call sites can pass `protocolVersion: '1.3.0'` and the
|
|
24
|
+
* pipeline emits the v1.3 mission record action (`dispatch.agent` +
|
|
25
|
+
* `dispatch.missionPacketPointer`) instead of the v1.2 `rolePackPointer`.
|
|
26
|
+
*/
|
|
27
|
+
function sessionHasMissionRole(session) {
|
|
28
|
+
return Boolean(session.roleMissionPackets && Object.keys(session.roleMissionPackets).length > 0);
|
|
29
|
+
}
|
|
20
30
|
function adapterSessionSummary(session) {
|
|
21
|
-
const { roleBriefs: _roleBriefs, rolePacks: _rolePacks, ...summary } = session;
|
|
31
|
+
const { roleBriefs: _roleBriefs, rolePacks: _rolePacks, roleMissionPackets: _roleMissionPackets, ...summary } = session;
|
|
22
32
|
return summary;
|
|
23
33
|
}
|
|
24
34
|
async function adapterHandoff(session) {
|
|
@@ -32,6 +42,10 @@ async function adapterHandoff(session) {
|
|
|
32
42
|
: 'finalize-required';
|
|
33
43
|
const protocol = await loadOperatingProtocol();
|
|
34
44
|
const handoff = protocol.createOperatingAdapterHandoff({
|
|
45
|
+
// v1.3 when any bound role resolved mission, so the record action names the
|
|
46
|
+
// generated lens agent and points at its mission packet; v1.2 (pack) default
|
|
47
|
+
// otherwise, byte-compatible with the pack-mode handoff.
|
|
48
|
+
...(sessionHasMissionRole(session) ? { protocolVersion: '1.3.0' } : {}),
|
|
35
49
|
phase: session.phase,
|
|
36
50
|
state,
|
|
37
51
|
cycleId: session.cycleId,
|
|
@@ -55,8 +69,66 @@ function sameRoleSet(left, right) {
|
|
|
55
69
|
return (left.length === right.length &&
|
|
56
70
|
[...left].sort().every((role, index) => role === [...right].sort()[index]));
|
|
57
71
|
}
|
|
58
|
-
function sessionExpired(session) {
|
|
59
|
-
return Date.parse(session.expiresAt) <=
|
|
72
|
+
function sessionExpired(session, nowMs = Date.now()) {
|
|
73
|
+
return Date.parse(session.expiresAt) <= nowMs;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* FR4: the board's identity is the hash of its event-chain genesis event (the
|
|
77
|
+
* event whose `previousEventHash` is null). It is immutable for a board
|
|
78
|
+
* generation — appending events never rewrites the genesis — and a board
|
|
79
|
+
* re-inited at the same path re-genesises the chain, so the identity changes.
|
|
80
|
+
* Machine-local adapter sessions are bound to it so a session from a superseded
|
|
81
|
+
* generation is never matched or reused by the current board. Returns '' when
|
|
82
|
+
* no committed chain exists yet, or if the chain cannot be read/verified (that
|
|
83
|
+
* failure is surfaced by the dedicated event-replay diagnostics, not here).
|
|
84
|
+
*/
|
|
85
|
+
async function committedBoardIdentity(store) {
|
|
86
|
+
try {
|
|
87
|
+
const { events } = await store.replay();
|
|
88
|
+
const genesis = events.find((event) => event.previousEventHash === null) ?? events[0];
|
|
89
|
+
return genesis?.eventHash ?? '';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function removeMachineLocalCacheDir(target) {
|
|
96
|
+
const entries = await readdir(target, { withFileTypes: true }).catch(() => []);
|
|
97
|
+
const removed = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.json')).length;
|
|
98
|
+
await rm(target, { recursive: true, force: true });
|
|
99
|
+
return removed;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* FR4: purge the board's machine-local advisor sessions (`<localRoot>/advisors/`)
|
|
103
|
+
* and incremental evidence baselines (`<localRoot>/evidence/incremental/`). A
|
|
104
|
+
* committed `operate init` apply calls this so a board re-inited at the same path
|
|
105
|
+
* never inherits a prior generation's sessions or cached baselines, and
|
|
106
|
+
* `operate cache purge` calls it so the doctor's staleness diagnostics have a
|
|
107
|
+
* scoped fix command. Both surfaces are rebuildable machine-local caches, never
|
|
108
|
+
* committed protocol artifacts.
|
|
109
|
+
*/
|
|
110
|
+
export async function purgeBoardMachineLocalCaches(input) {
|
|
111
|
+
const paths = resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot });
|
|
112
|
+
const removedAdvisorSessions = await removeMachineLocalCacheDir(paths.advisors);
|
|
113
|
+
const removedIncrementalBaselines = await removeMachineLocalCacheDir(path.join(paths.evidence, 'incremental'));
|
|
114
|
+
return { removedAdvisorSessions, removedIncrementalBaselines };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Surface the adapter session lease ergonomically (FR10 / T-008): the absolute
|
|
118
|
+
* `expiresAt` plus the remaining time relative to the resolved clock. Included in
|
|
119
|
+
* every adapter lifecycle response so a native runtime can see, at a glance, how
|
|
120
|
+
* long its prepared session is still valid and when it must resume or re-run —
|
|
121
|
+
* rather than parsing `expiresAt` against wall-clock itself. `remainingMs` is
|
|
122
|
+
* floored at zero so an already-lapsed lease reads as `expired`, never negative.
|
|
123
|
+
*/
|
|
124
|
+
function adapterLeaseStatus(session, nowMs) {
|
|
125
|
+
const remainingMs = Math.max(0, Date.parse(session.expiresAt) - nowMs);
|
|
126
|
+
return {
|
|
127
|
+
expiresAt: session.expiresAt,
|
|
128
|
+
remainingMs,
|
|
129
|
+
remainingSeconds: Math.floor(remainingMs / 1_000),
|
|
130
|
+
expired: remainingMs <= 0,
|
|
131
|
+
};
|
|
60
132
|
}
|
|
61
133
|
function retryRunCommand(session) {
|
|
62
134
|
return `planr operate run --cycle-id ${session.cycleId} --runtime ${session.runtime} --json`;
|
|
@@ -148,11 +220,24 @@ export async function operatingCacheAction(input) {
|
|
|
148
220
|
localRoot: input.localRoot,
|
|
149
221
|
purge: true,
|
|
150
222
|
});
|
|
223
|
+
// FR4/FR11: clearing machine-local caches also drops board-bound adapter
|
|
224
|
+
// sessions and incremental evidence baselines, so this is the scoped fix the
|
|
225
|
+
// doctor names for stale adapter sessions and stale incremental baselines.
|
|
226
|
+
const board = await purgeBoardMachineLocalCaches({
|
|
227
|
+
projectRoot: input.projectRoot,
|
|
228
|
+
localRoot: input.localRoot,
|
|
229
|
+
});
|
|
151
230
|
return {
|
|
152
|
-
removed: removed.length +
|
|
231
|
+
removed: removed.length +
|
|
232
|
+
sessions.removed +
|
|
233
|
+
classifications.purged +
|
|
234
|
+
board.removedAdvisorSessions +
|
|
235
|
+
board.removedIncrementalBaselines,
|
|
153
236
|
evidence: { removed: removed.length, entries: removed },
|
|
154
237
|
sessions,
|
|
155
238
|
classifications,
|
|
239
|
+
adapterSessions: { removed: board.removedAdvisorSessions },
|
|
240
|
+
incrementalBaselines: { removed: board.removedIncrementalBaselines },
|
|
156
241
|
};
|
|
157
242
|
}
|
|
158
243
|
function integrityKeyPath(projectRoot, localRoot) {
|
|
@@ -559,7 +644,7 @@ export async function readPersistedOperatingRoleResults(store, cycleId) {
|
|
|
559
644
|
}
|
|
560
645
|
return [...byRole.values()].sort((left, right) => left.roleId.localeCompare(right.roleId));
|
|
561
646
|
}
|
|
562
|
-
async function readAdapterSession(projectRoot, cycleId, localRoot) {
|
|
647
|
+
async function readAdapterSession(projectRoot, cycleId, localRoot, nowMs = Date.now()) {
|
|
563
648
|
const parsed = JSON.parse(await readFile(adapterSessionPath(projectRoot, cycleId, localRoot), 'utf8'));
|
|
564
649
|
const session = {
|
|
565
650
|
...parsed,
|
|
@@ -569,7 +654,10 @@ async function readAdapterSession(projectRoot, cycleId, localRoot) {
|
|
|
569
654
|
if (session.implementation !== 'openplanr-operate-adapter') {
|
|
570
655
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Adapter session is invalid.');
|
|
571
656
|
}
|
|
572
|
-
|
|
657
|
+
// Expiry is evaluated against the resolved clock (an injected clock in tests,
|
|
658
|
+
// wall-clock in production) so a lease that lapsed after its refresh window is
|
|
659
|
+
// still rejected here even when the caller supplies a deterministic clock.
|
|
660
|
+
if (sessionExpired(session, nowMs)) {
|
|
573
661
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Adapter session expired.', {
|
|
574
662
|
cycleId,
|
|
575
663
|
recoveryCommand: retryRunCommand(session),
|
|
@@ -600,6 +688,81 @@ async function assertAdapterCycleBinding(input, session) {
|
|
|
600
688
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter evidence no longer matches the committed cycle snapshot.');
|
|
601
689
|
}
|
|
602
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* The machine-local sensitivity ceiling (default `internal`), read the same way
|
|
693
|
+
* the evidence cache and cycle engine read it. Mission packets and their tool
|
|
694
|
+
* grants are narrowed to this ceiling.
|
|
695
|
+
*/
|
|
696
|
+
async function readAdapterSensitivityCeiling(projectRoot, localRoot) {
|
|
697
|
+
const paths = resolveOperatingPaths(projectRoot, { localRoot });
|
|
698
|
+
return readFile(path.join(paths.localRoot, 'preferences.json'), 'utf8')
|
|
699
|
+
.then((raw) => JSON.parse(raw).sensitivityCeiling ??
|
|
700
|
+
'internal')
|
|
701
|
+
.catch(() => 'internal');
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* FR2: deny-list every declared read root that contains an above-ceiling evidence
|
|
705
|
+
* item BEFORE the mission packet is built, so no above-ceiling file is reachable
|
|
706
|
+
* even inside a granted root. Uses `mission-dispatch.ts`'s
|
|
707
|
+
* `narrowMissionRootsToCeiling` over the FULL (pre-ceiling-filter) evidence, then
|
|
708
|
+
* drops the items in the denied roots. `buildOperatingMissionPackets` then derives
|
|
709
|
+
* the packet's `declaredRoots` (and thus its tool grant) from only the narrowed
|
|
710
|
+
* roots. This is the belt to the bounded reader's read-time-ceiling suspenders.
|
|
711
|
+
*/
|
|
712
|
+
function narrowEvidenceToMissionCeiling(evidence, ceiling) {
|
|
713
|
+
const topSegment = (location) => {
|
|
714
|
+
const [top] = location.split('/');
|
|
715
|
+
return top && top.length > 0 ? top : null;
|
|
716
|
+
};
|
|
717
|
+
const declaredRoots = [
|
|
718
|
+
...new Set(evidence.items
|
|
719
|
+
.map((item) => topSegment(item.location))
|
|
720
|
+
.filter((segment) => Boolean(segment))),
|
|
721
|
+
];
|
|
722
|
+
const evidenceIndex = evidence.items.map((item) => ({
|
|
723
|
+
path: item.location,
|
|
724
|
+
sensitivity: item.sensitivity,
|
|
725
|
+
}));
|
|
726
|
+
const allowedRoots = new Set(narrowMissionRootsToCeiling({ declaredRoots, evidenceIndex, ceiling }));
|
|
727
|
+
return {
|
|
728
|
+
...evidence,
|
|
729
|
+
items: evidence.items.filter((item) => {
|
|
730
|
+
const top = topSegment(item.location);
|
|
731
|
+
return top === null || allowedRoots.has(top);
|
|
732
|
+
}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Resolve every requested role's effective dispatch mode for THIS runtime (FR1):
|
|
737
|
+
* the v1.3 registry default, overridden by the machine-local
|
|
738
|
+
* `dispatchModeOverrides`, reconciled against whether the runtime natively
|
|
739
|
+
* enforces the bounded read-only boundary. The adapter lifecycle IS the native
|
|
740
|
+
* runtime path, so the hosting adapter is native-capable by construction; the
|
|
741
|
+
* runtime's own enforceability then decides whether a role receives a native
|
|
742
|
+
* bounded mission lens (`resolution.native`) or falls closed to the v1.2 pack.
|
|
743
|
+
*/
|
|
744
|
+
async function resolveBoundRoleDispatchModes(input) {
|
|
745
|
+
const protocol = await loadOperatingProtocol();
|
|
746
|
+
const registryDefaults = new Map(protocol.listOperatingRoles().map((role) => [
|
|
747
|
+
role.id,
|
|
748
|
+
operatingRegistryDispatchMode(role),
|
|
749
|
+
]));
|
|
750
|
+
const overrides = await readOperatingDispatchModeOverrides(input.projectRoot, {
|
|
751
|
+
localRoot: input.localRoot,
|
|
752
|
+
});
|
|
753
|
+
const runtimeEnforcesBoundedReadOnly = await operatingRuntimeEnforcesBoundedReadOnly(input.runtime);
|
|
754
|
+
const resolved = new Map();
|
|
755
|
+
for (const roleId of input.roles) {
|
|
756
|
+
resolved.set(roleId, resolveOperatingDispatchMode({
|
|
757
|
+
roleId: roleId,
|
|
758
|
+
registryDefault: registryDefaults.get(roleId) ?? 'mission',
|
|
759
|
+
override: overrides[roleId],
|
|
760
|
+
runtimeEnforcesBoundedReadOnly,
|
|
761
|
+
adapterNativeCapable: true,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
return resolved;
|
|
765
|
+
}
|
|
603
766
|
export async function createOperatingAdapterStartHandoff(input) {
|
|
604
767
|
const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
|
|
605
768
|
const existing = await readFile(target, 'utf8')
|
|
@@ -625,7 +788,19 @@ export async function createOperatingAdapterStartHandoff(input) {
|
|
|
625
788
|
}
|
|
626
789
|
}
|
|
627
790
|
const protocol = await loadOperatingProtocol();
|
|
791
|
+
// FR1 call site 2 (the start / prepare-required handoff): stamp v1.3 whenever
|
|
792
|
+
// any bound role would resolve to native mission dispatch on this runtime, so
|
|
793
|
+
// the whole lifecycle — including the record actions built after prepare —
|
|
794
|
+
// carries the mission protocol version rather than defaulting to pack.
|
|
795
|
+
const dispatchModes = await resolveBoundRoleDispatchModes({
|
|
796
|
+
projectRoot: input.projectRoot,
|
|
797
|
+
localRoot: input.localRoot,
|
|
798
|
+
runtime: input.runtime,
|
|
799
|
+
roles: input.roles,
|
|
800
|
+
});
|
|
801
|
+
const anyMission = [...dispatchModes.values()].some((resolution) => resolution.native);
|
|
628
802
|
const handoff = protocol.createOperatingAdapterHandoff({
|
|
803
|
+
...(anyMission ? { protocolVersion: '1.3.0' } : {}),
|
|
629
804
|
phase: input.phase,
|
|
630
805
|
state: 'prepare-required',
|
|
631
806
|
cycleId: input.cycleId,
|
|
@@ -646,6 +821,13 @@ export async function operateAdapterLifecycle(input) {
|
|
|
646
821
|
if (!input.cycleId || !input.idempotencyKey) {
|
|
647
822
|
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter calls require --cycle-id and --idempotency-key.');
|
|
648
823
|
}
|
|
824
|
+
const nowMs = (input.now?.() ?? new Date()).getTime();
|
|
825
|
+
// The lease window is a machine-local preference (default 15 minutes). Resolved
|
|
826
|
+
// once so both the fresh `prepare` expiry and the per-`record` refresh use the
|
|
827
|
+
// same configured duration.
|
|
828
|
+
const leaseDurationMs = await readOperatingAdapterLeaseDurationMs(input.projectRoot, {
|
|
829
|
+
localRoot: input.localRoot,
|
|
830
|
+
});
|
|
649
831
|
const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
|
|
650
832
|
if (input.action === 'prepare') {
|
|
651
833
|
if (!input.evidenceDigest?.startsWith('sha256:')) {
|
|
@@ -655,6 +837,9 @@ export async function operateAdapterLifecycle(input) {
|
|
|
655
837
|
localRoot: input.localRoot,
|
|
656
838
|
});
|
|
657
839
|
const state = await store.state();
|
|
840
|
+
// FR4: the identity of the board that owns this prepare. A machine-local
|
|
841
|
+
// session bound to any other genesis belongs to a superseded generation.
|
|
842
|
+
const boardIdentity = await committedBoardIdentity(store);
|
|
658
843
|
const cycle = state.cycles.find((record) => record.id === input.cycleId);
|
|
659
844
|
if (!cycle || !['advising', 'blocked'].includes(cycle.state)) {
|
|
660
845
|
throw new OperateError('E_OPERATE_STATE_INVALID', 'Adapter prepare requires an advising or blocked cycle.');
|
|
@@ -693,36 +878,63 @@ export async function operateAdapterLifecycle(input) {
|
|
|
693
878
|
const existing = await readFile(target, 'utf8')
|
|
694
879
|
.then((raw) => JSON.parse(raw))
|
|
695
880
|
.catch(() => null);
|
|
881
|
+
// Recovery semantics (FR10 / T-008, behavior unchanged — documented here so
|
|
882
|
+
// the lease ergonomics read coherently): when a prior session for this cycle
|
|
883
|
+
// exists and its binding is an *exact* match (same cycle, evidence digest,
|
|
884
|
+
// runtime, phase, and role set) but it has lapsed or was cancelled, its
|
|
885
|
+
// deterministic role packs are reused rather than rebuilt. The reused packs
|
|
886
|
+
// carry the same per-role `inputDigest`, so any machine-local role result that
|
|
887
|
+
// still matches that digest is re-adopted below as already-recorded work — the
|
|
888
|
+
// lease is reissued fresh (new token + refreshed `expiresAt`) while the proven,
|
|
889
|
+
// digest-bound advisory output is preserved. A non-exact prior binding is never
|
|
890
|
+
// recovered; it fails closed above with `E_OPERATE_ADVISOR_ISOLATION`.
|
|
696
891
|
let recoverableSession = null;
|
|
697
892
|
if (existing) {
|
|
698
893
|
const normalized = {
|
|
699
894
|
...existing,
|
|
700
895
|
phase: existing.phase ?? adapterPhase(existing.roles ?? []),
|
|
701
896
|
runtime: existing.runtime ?? 'auto',
|
|
897
|
+
boardIdentity: existing.boardIdentity ?? '',
|
|
702
898
|
};
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
899
|
+
// FR4: a session bound to a superseded board generation (its genesis no
|
|
900
|
+
// longer matches the committed event chain) never blocks, matches, or is
|
|
901
|
+
// reused by the current board — it is silently superseded by the fresh
|
|
902
|
+
// session written below. This is what lets a board re-inited at the same
|
|
903
|
+
// path — whose CYCLE-NNN ordinal collides with a prior generation's
|
|
904
|
+
// finalized session — prepare cleanly instead of dead-ending on
|
|
905
|
+
// `E_OPERATE_ADVISOR_ISOLATION` / a whole-cycle cancel.
|
|
906
|
+
const sessionMatchesBoard = normalized.boardIdentity === boardIdentity;
|
|
907
|
+
if (sessionMatchesBoard) {
|
|
908
|
+
const exact = normalized.cycleId === input.cycleId &&
|
|
909
|
+
normalized.evidenceDigest === input.evidenceDigest &&
|
|
910
|
+
normalized.runtime === runtime &&
|
|
911
|
+
normalized.phase === phase &&
|
|
912
|
+
sameRoleSet(normalized.roles, roles);
|
|
913
|
+
if (normalized.idempotencyKey === input.idempotencyKey) {
|
|
914
|
+
if (!exact) {
|
|
915
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Idempotent adapter prepare does not match its original cycle, evidence, runtime, phase, or role binding.');
|
|
916
|
+
}
|
|
917
|
+
if (sessionExpired(normalized) || normalized.state === 'cancelled') {
|
|
918
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'The prepared adapter session is expired or cancelled; request a fresh CLI-owned handoff.', { recoveryCommand: retryRunCommand(normalized) });
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
...normalized,
|
|
922
|
+
leaseStatus: adapterLeaseStatus(normalized, nowMs),
|
|
923
|
+
handoff: await adapterHandoff(normalized),
|
|
924
|
+
};
|
|
711
925
|
}
|
|
712
|
-
if (sessionExpired(normalized)
|
|
713
|
-
throw new OperateError('
|
|
926
|
+
if (!sessionExpired(normalized) && ['prepared', 'recording'].includes(normalized.state)) {
|
|
927
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already has an active adapter session with another binding.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
928
|
+
}
|
|
929
|
+
// FR4: a finalized/expired/cancelled same-board session no longer forces
|
|
930
|
+
// a whole-cycle-cancelling error on a new compatible prepare. The
|
|
931
|
+
// advisors→chair continuation and a same-phase re-prepare both fall
|
|
932
|
+
// through and supersede the dead session; an exact dead session still
|
|
933
|
+
// recovers its already-built packs so recorded advisory work is reused
|
|
934
|
+
// rather than recomputed.
|
|
935
|
+
if (exact && (sessionExpired(normalized) || normalized.state === 'cancelled')) {
|
|
936
|
+
recoverableSession = normalized;
|
|
714
937
|
}
|
|
715
|
-
return { ...normalized, handoff: await adapterHandoff(normalized) };
|
|
716
|
-
}
|
|
717
|
-
if (!sessionExpired(normalized) && ['prepared', 'recording'].includes(normalized.state)) {
|
|
718
|
-
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already has an active adapter session with another binding.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
719
|
-
}
|
|
720
|
-
if (normalized.state === 'finalized' &&
|
|
721
|
-
!(normalized.phase === 'advisors' && phase === 'chair')) {
|
|
722
|
-
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already finalized its ${normalized.phase} adapter phase.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
723
|
-
}
|
|
724
|
-
if (exact && (sessionExpired(normalized) || normalized.state === 'cancelled')) {
|
|
725
|
-
recoverableSession = normalized;
|
|
726
938
|
}
|
|
727
939
|
}
|
|
728
940
|
const roleEvidence = roles[0] === 'chair'
|
|
@@ -748,8 +960,22 @@ export async function operateAdapterLifecycle(input) {
|
|
|
748
960
|
state,
|
|
749
961
|
cycleId: input.cycleId,
|
|
750
962
|
});
|
|
963
|
+
// FR1: resolve each requested role's dispatch mode for THIS runtime. A role
|
|
964
|
+
// that resolves to a native bounded lens (`resolution.native`) gets a
|
|
965
|
+
// body-free v1.3 mission packet; every other role keeps the v1.2 role pack.
|
|
966
|
+
// A recovered dead-but-exact session reuses its stored packs/packets verbatim.
|
|
967
|
+
const dispatchModes = await resolveBoundRoleDispatchModes({
|
|
968
|
+
projectRoot: input.projectRoot,
|
|
969
|
+
localRoot: input.localRoot,
|
|
970
|
+
runtime,
|
|
971
|
+
roles,
|
|
972
|
+
});
|
|
973
|
+
const missionRoleIds = readiness.roles
|
|
974
|
+
.map((role) => role.roleId)
|
|
975
|
+
.filter((roleId) => dispatchModes.get(roleId)?.native);
|
|
976
|
+
const packReadinessRoles = readiness.roles.filter((role) => !dispatchModes.get(role.roleId)?.native);
|
|
751
977
|
const rolePacks = recoverableSession?.rolePacks ??
|
|
752
|
-
Object.fromEntries(await Promise.all(
|
|
978
|
+
Object.fromEntries(await Promise.all(packReadinessRoles.map(async (role) => [
|
|
753
979
|
role.roleId,
|
|
754
980
|
await createOperatingAdvisorPack({
|
|
755
981
|
cycleId: input.cycleId,
|
|
@@ -758,7 +984,66 @@ export async function operateAdapterLifecycle(input) {
|
|
|
758
984
|
context,
|
|
759
985
|
}),
|
|
760
986
|
])));
|
|
761
|
-
|
|
987
|
+
// FR1: build the natively-dispatched roles' mission packets from live
|
|
988
|
+
// cycle/workspace state. Only reached when a role actually resolved mission,
|
|
989
|
+
// so a pure pack-mode prepare never loads config/workspace and stays
|
|
990
|
+
// byte-compatible with the v1.2 path. The evidence is ceiling-narrowed by
|
|
991
|
+
// `narrowMissionRootsToCeiling` first, so a declared read root can never reach
|
|
992
|
+
// an above-ceiling file even before the bounded reader's read-time check.
|
|
993
|
+
const roleMissionPackets = recoverableSession?.roleMissionPackets ??
|
|
994
|
+
(missionRoleIds.length > 0
|
|
995
|
+
? await (async () => {
|
|
996
|
+
const [config, workspace, sensitivityCeiling] = await Promise.all([
|
|
997
|
+
readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }),
|
|
998
|
+
refreshOperatingWorkspaceManifest(input.projectRoot, { localRoot: input.localRoot }),
|
|
999
|
+
readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
|
|
1000
|
+
]);
|
|
1001
|
+
const packets = await buildOperatingMissionPackets({
|
|
1002
|
+
cycleId: input.cycleId,
|
|
1003
|
+
config,
|
|
1004
|
+
workspace,
|
|
1005
|
+
context,
|
|
1006
|
+
evidence: narrowEvidenceToMissionCeiling(roleEvidence, sensitivityCeiling),
|
|
1007
|
+
roleIds: missionRoleIds,
|
|
1008
|
+
sensitivityCeiling,
|
|
1009
|
+
});
|
|
1010
|
+
return Object.fromEntries(packets.map((packet) => [packet.roleId, packet]));
|
|
1011
|
+
})()
|
|
1012
|
+
: {});
|
|
1013
|
+
// FR2: fail closed before a native adapter session is assembled. A freshly
|
|
1014
|
+
// built pack is already measured inside `createOperatingAdvisorPack`, but the
|
|
1015
|
+
// session may instead be RECOVERED from disk (`recoverableSession`) — possibly
|
|
1016
|
+
// written before pack-budget enforcement existed. Re-measure every pack that
|
|
1017
|
+
// will back this session against its role's published v1.2 `maxInputBytes`, so
|
|
1018
|
+
// an oversized pack can never reach the adapter from either source instead of
|
|
1019
|
+
// silently constructing an oversized session. (Mission packets are measured
|
|
1020
|
+
// against their derived mission budget inside `buildOperatingMissionPackets`.)
|
|
1021
|
+
const packInputBudgets = new Map(protocol.listOperatingRoles().map((role) => {
|
|
1022
|
+
const maxInputBytes = role.budgets
|
|
1023
|
+
?.maxInputBytes;
|
|
1024
|
+
return [role.id, typeof maxInputBytes === 'number' ? maxInputBytes : 262_144];
|
|
1025
|
+
}));
|
|
1026
|
+
for (const [roleId, pack] of Object.entries(rolePacks)) {
|
|
1027
|
+
assertOperatingAdvisorPackWithinBudget(pack, packInputBudgets.get(roleId) ?? 262_144);
|
|
1028
|
+
}
|
|
1029
|
+
// A mission role's brief facet is the same registry-derived brief pack mode
|
|
1030
|
+
// uses, so the record path's brief-bound machinery (output limits, contract
|
|
1031
|
+
// details, output-vs-brief checks) applies uniformly across both modes.
|
|
1032
|
+
const roleBriefs = {
|
|
1033
|
+
...Object.fromEntries(Object.entries(rolePacks).map(([role, pack]) => [role, pack.roleBrief])),
|
|
1034
|
+
...Object.fromEntries(Object.keys(roleMissionPackets).map((role) => [
|
|
1035
|
+
role,
|
|
1036
|
+
protocol.createOperatingAdvisorBrief(role),
|
|
1037
|
+
])),
|
|
1038
|
+
};
|
|
1039
|
+
// A role's committed input digest is its pack's `inputDigest` (pack mode) or
|
|
1040
|
+
// its mission packet's `packetDigest` (mission mode). The record path binds a
|
|
1041
|
+
// recorded result to exactly this value, and a recovered machine-local result
|
|
1042
|
+
// is re-adopted only when it still matches it.
|
|
1043
|
+
const roleInputDigests = Object.fromEntries(roles.map((role) => {
|
|
1044
|
+
const digest = roleMissionPackets[role]?.packetDigest ?? rolePacks[role]?.inputDigest;
|
|
1045
|
+
return [role, digest];
|
|
1046
|
+
}));
|
|
762
1047
|
const validRecordedRoles = [];
|
|
763
1048
|
for (const role of roles) {
|
|
764
1049
|
const prior = await readFile(path.join(path.dirname(target), `${input.cycleId}.${role}.json`), 'utf8')
|
|
@@ -767,7 +1052,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
767
1052
|
if (prior &&
|
|
768
1053
|
prior.cycleId === input.cycleId &&
|
|
769
1054
|
prior.roleId === role &&
|
|
770
|
-
prior.inputDigest ===
|
|
1055
|
+
prior.inputDigest === roleInputDigests[role]) {
|
|
771
1056
|
try {
|
|
772
1057
|
await assertOperatingArtifact('operating-role-result', prior);
|
|
773
1058
|
protocol.validateOperatingRoleResultDigest(prior);
|
|
@@ -781,6 +1066,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
781
1066
|
}
|
|
782
1067
|
const session = {
|
|
783
1068
|
implementation: 'openplanr-operate-adapter',
|
|
1069
|
+
boardIdentity,
|
|
784
1070
|
cycleId: input.cycleId,
|
|
785
1071
|
evidenceDigest: input.evidenceDigest,
|
|
786
1072
|
phase,
|
|
@@ -788,20 +1074,31 @@ export async function operateAdapterLifecycle(input) {
|
|
|
788
1074
|
lease: randomBytes(32).toString('base64url'),
|
|
789
1075
|
idempotencyKey: input.idempotencyKey,
|
|
790
1076
|
state: validRecordedRoles.length > 0 ? 'recording' : 'prepared',
|
|
791
|
-
expiresAt: new Date(
|
|
1077
|
+
expiresAt: new Date(nowMs + leaseDurationMs).toISOString(),
|
|
792
1078
|
roles,
|
|
793
1079
|
recordedRoles: validRecordedRoles.sort(),
|
|
794
1080
|
roleBriefs,
|
|
795
1081
|
rolePacks,
|
|
796
|
-
|
|
1082
|
+
// Omitted entirely for a pure pack-mode session so its on-disk shape stays
|
|
1083
|
+
// byte-compatible with the v1.2 path.
|
|
1084
|
+
...(Object.keys(roleMissionPackets).length > 0 ? { roleMissionPackets } : {}),
|
|
1085
|
+
roleInputDigests,
|
|
797
1086
|
};
|
|
798
1087
|
await atomicPrivateWrite(target, session);
|
|
799
|
-
return {
|
|
1088
|
+
return {
|
|
1089
|
+
...session,
|
|
1090
|
+
// Expose the mission packets at `/data/missionPackets/<role>` so the v1.3
|
|
1091
|
+
// record action's `dispatch.missionPacketPointer` resolves against the
|
|
1092
|
+
// prepare result; pack roles keep resolving at `/data/rolePacks/<role>`.
|
|
1093
|
+
...(Object.keys(roleMissionPackets).length > 0 ? { missionPackets: roleMissionPackets } : {}),
|
|
1094
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
1095
|
+
handoff: await adapterHandoff(session),
|
|
1096
|
+
};
|
|
800
1097
|
}
|
|
801
1098
|
if (!input.lease) {
|
|
802
1099
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter lease is required.');
|
|
803
1100
|
}
|
|
804
|
-
const session = await readAdapterSession(input.projectRoot, input.cycleId, input.localRoot);
|
|
1101
|
+
const session = await readAdapterSession(input.projectRoot, input.cycleId, input.localRoot, nowMs);
|
|
805
1102
|
assertAdapterBinding(session, input.lease, input.idempotencyKey, input.evidenceDigest);
|
|
806
1103
|
await assertAdapterCycleBinding({
|
|
807
1104
|
projectRoot: input.projectRoot,
|
|
@@ -812,7 +1109,11 @@ export async function operateAdapterLifecycle(input) {
|
|
|
812
1109
|
if (!['prepared', 'recording'].includes(session.state)) {
|
|
813
1110
|
throw new OperateError('E_OPERATE_STATE_INVALID', `Adapter resume is not valid after the session is ${session.state}.`, { recoveryCommand: retryRunCommand(session) });
|
|
814
1111
|
}
|
|
815
|
-
return {
|
|
1112
|
+
return {
|
|
1113
|
+
...session,
|
|
1114
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
1115
|
+
handoff: await adapterHandoff(session),
|
|
1116
|
+
};
|
|
816
1117
|
}
|
|
817
1118
|
if (input.action === 'cancel') {
|
|
818
1119
|
if (session.state === 'finalized') {
|
|
@@ -821,6 +1122,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
821
1122
|
if (session.state === 'cancelled') {
|
|
822
1123
|
return {
|
|
823
1124
|
session: adapterSessionSummary(session),
|
|
1125
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
824
1126
|
handoff: await adapterHandoff(session),
|
|
825
1127
|
};
|
|
826
1128
|
}
|
|
@@ -828,6 +1130,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
828
1130
|
await atomicPrivateWrite(target, cancelled);
|
|
829
1131
|
return {
|
|
830
1132
|
session: adapterSessionSummary(cancelled),
|
|
1133
|
+
leaseStatus: adapterLeaseStatus(cancelled, nowMs),
|
|
831
1134
|
handoff: await adapterHandoff(cancelled),
|
|
832
1135
|
};
|
|
833
1136
|
}
|
|
@@ -870,6 +1173,9 @@ export async function operateAdapterLifecycle(input) {
|
|
|
870
1173
|
}
|
|
871
1174
|
const submittedRecord = submitted && typeof submitted === 'object' ? submitted : {};
|
|
872
1175
|
let result;
|
|
1176
|
+
// FR1: unresolvable-citation gaps opened while resolving a mission response's
|
|
1177
|
+
// citations, surfaced in the record response for observability.
|
|
1178
|
+
let recordGaps = [];
|
|
873
1179
|
if (submittedRecord.kind === 'operating-role-result') {
|
|
874
1180
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Native advisors must submit only the compact response contract; OpenPlanr owns canonical result metadata.', {
|
|
875
1181
|
...advisorResponseContractDetails(session.roleBriefs[input.role]),
|
|
@@ -898,12 +1204,50 @@ export async function operateAdapterLifecycle(input) {
|
|
|
898
1204
|
throw new OperateError('E_OPERATE_SECRET_DETECTED', `Native advisor response contains a secret at ${field.location}; nothing was persisted.`, { roleId: input.role, field: field.location });
|
|
899
1205
|
}
|
|
900
1206
|
}
|
|
1207
|
+
const missionPacket = session.roleMissionPackets?.[input.role];
|
|
901
1208
|
try {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1209
|
+
if (missionPacket) {
|
|
1210
|
+
// FR1: a mission-mode role submits the v1.3 citation-bearing response.
|
|
1211
|
+
// Its citations flow into the engine's already-live citation gate,
|
|
1212
|
+
// which resolves them against the cycle's pinned revision and mints the
|
|
1213
|
+
// evidenceRefs that make the committed result v1.2-valid — the pack
|
|
1214
|
+
// path is untouched.
|
|
1215
|
+
const gated = await createNativeMissionOperatingRoleResult({
|
|
1216
|
+
packet: missionPacket,
|
|
1217
|
+
response: submitted,
|
|
1218
|
+
runtime: session.runtime,
|
|
1219
|
+
resolveCitations: async (roleResults) => {
|
|
1220
|
+
const [workspace, sensitivityCeiling, config] = await Promise.all([
|
|
1221
|
+
refreshOperatingWorkspaceManifest(input.projectRoot, {
|
|
1222
|
+
localRoot: input.localRoot,
|
|
1223
|
+
}),
|
|
1224
|
+
readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
|
|
1225
|
+
readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }).catch(() => null),
|
|
1226
|
+
]);
|
|
1227
|
+
return gateRecordedProposalCitations({
|
|
1228
|
+
roleResults,
|
|
1229
|
+
context: {
|
|
1230
|
+
projectRoot: input.projectRoot,
|
|
1231
|
+
cycleId: input.cycleId,
|
|
1232
|
+
descriptor: workspace.controlRepository,
|
|
1233
|
+
cache: new OperatingEvidenceCache(resolveOperatingPaths(input.projectRoot, {
|
|
1234
|
+
localRoot: input.localRoot,
|
|
1235
|
+
}).evidence, sensitivityCeiling),
|
|
1236
|
+
owner: config?.decisionOwner,
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
result = gated.result;
|
|
1242
|
+
recordGaps = gated.gaps;
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
result = await createNativeOperatingRoleResult({
|
|
1246
|
+
pack: session.rolePacks[input.role],
|
|
1247
|
+
response: submitted,
|
|
1248
|
+
runtime: session.runtime,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
907
1251
|
}
|
|
908
1252
|
catch (error) {
|
|
909
1253
|
if (error instanceof OperateError && error.code === 'E_OPERATE_ADVISOR_FAILED') {
|
|
@@ -965,16 +1309,23 @@ export async function operateAdapterLifecycle(input) {
|
|
|
965
1309
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Role ${input.role} already recorded a different result.`);
|
|
966
1310
|
}
|
|
967
1311
|
await atomicPrivateWrite(path.join(path.dirname(target), `${input.cycleId}.${input.role}.json`), result);
|
|
1312
|
+
// A successful record refreshes the lease forward from now (FR10 / T-008):
|
|
1313
|
+
// an advisor making steady progress across a multi-role dispatch keeps its
|
|
1314
|
+
// session alive without a separate keep-alive call, while a session that goes
|
|
1315
|
+
// idle past the refreshed window still lapses and is rejected on the next call.
|
|
968
1316
|
const updated = {
|
|
969
1317
|
...session,
|
|
970
1318
|
state: 'recording',
|
|
971
1319
|
recordedRoles: [...new Set([...session.recordedRoles, input.role])].sort(),
|
|
1320
|
+
expiresAt: new Date(nowMs + leaseDurationMs).toISOString(),
|
|
972
1321
|
};
|
|
973
1322
|
await atomicPrivateWrite(target, updated);
|
|
974
1323
|
return {
|
|
975
1324
|
recorded: input.role,
|
|
976
1325
|
result,
|
|
1326
|
+
...(recordGaps.length > 0 ? { citationGaps: recordGaps } : {}),
|
|
977
1327
|
session: adapterSessionSummary(updated),
|
|
1328
|
+
leaseStatus: adapterLeaseStatus(updated, nowMs),
|
|
978
1329
|
handoff: await adapterHandoff(updated),
|
|
979
1330
|
};
|
|
980
1331
|
}
|
|
@@ -994,6 +1345,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
994
1345
|
return {
|
|
995
1346
|
session: adapterSessionSummary(session),
|
|
996
1347
|
results: summaries,
|
|
1348
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
997
1349
|
handoff: await adapterHandoff(session),
|
|
998
1350
|
};
|
|
999
1351
|
}
|
|
@@ -1065,6 +1417,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
1065
1417
|
inputDigest: result.inputDigest,
|
|
1066
1418
|
resultDigest: result.resultDigest,
|
|
1067
1419
|
})),
|
|
1420
|
+
leaseStatus: adapterLeaseStatus(finalized, nowMs),
|
|
1068
1421
|
handoff: await adapterHandoff(finalized),
|
|
1069
1422
|
};
|
|
1070
1423
|
}
|