@indigoai-us/hq-cli 5.250.0 → 5.251.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/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.251.0] — 2026-09-27
|
|
6
|
+
|
|
5
7
|
## [5.250.0] — 2026-09-27
|
|
6
8
|
|
|
7
9
|
### Fixed
|
|
8
10
|
|
|
11
|
+
- A Work Mesh session bound to a project and task keeps the binding episode the
|
|
9
12
|
- An enrolled agent that sets `HQ_AGENT_DIR` now acts as that agent in every
|
|
10
13
|
command, not only under `hq agent`. `hq dm` and `hq secrets` used to look
|
|
11
14
|
for a person's session instead, find none, and open a browser sign-in. On
|
|
@@ -37,7 +40,6 @@
|
|
|
37
40
|
host key is missing, instead of passing on a cached token and leaving the
|
|
38
41
|
agent to fail at its next refresh.
|
|
39
42
|
|
|
40
|
-
|
|
41
43
|
## [5.249.1] — 2026-09-27
|
|
42
44
|
|
|
43
45
|
## [5.249.0] — 2026-09-27
|
|
@@ -8,7 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
9
9
|
import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
|
|
10
10
|
import { NotTrackingError } from "./errors.js";
|
|
11
|
-
import { readSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
|
|
11
|
+
import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
|
|
12
12
|
import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
|
|
13
13
|
/** Fields allowed on a durable outbox operation (privacy allowlist). */
|
|
14
14
|
export const OUTBOX_ALLOWLIST = [
|
|
@@ -285,7 +285,35 @@ export function writeAckedRegisterState(op, root, receiptId, now = () => new Dat
|
|
|
285
285
|
/** Repair older unresolved session projections from acknowledged registrations. */
|
|
286
286
|
export function repairAckedRegisterStates(root, sessionIds, now = () => new Date()) {
|
|
287
287
|
let repaired = 0;
|
|
288
|
-
|
|
288
|
+
const operations = listOutboxOperations(root);
|
|
289
|
+
// A stored bindingEpisodeId that is a register receipt makes the server drop
|
|
290
|
+
// every batch. Clear it even when the register operation that produced it is
|
|
291
|
+
// no longer in the outbox: the id shape identifies it. This runs for every
|
|
292
|
+
// requested session, whatever its company or tracking status.
|
|
293
|
+
const receiptsBySession = new Map();
|
|
294
|
+
for (const op of operations) {
|
|
295
|
+
if (op.kind !== "register" || !op.receiptId)
|
|
296
|
+
continue;
|
|
297
|
+
const receipts = receiptsBySession.get(op.sessionId) ?? [];
|
|
298
|
+
receipts.push(op.receiptId);
|
|
299
|
+
receiptsBySession.set(op.sessionId, receipts);
|
|
300
|
+
}
|
|
301
|
+
for (const sessionId of sessionIds ?? receiptsBySession.keys()) {
|
|
302
|
+
if (!isSafeWorkContextSegment(sessionId))
|
|
303
|
+
continue;
|
|
304
|
+
const known = receiptsBySession.get(sessionId) ?? [];
|
|
305
|
+
// Take the lock only for a session that needs the repair.
|
|
306
|
+
const observed = readSessionState(sessionId, root);
|
|
307
|
+
if (!observed || stripRegisterReceiptEpisodes(observed, known) === observed)
|
|
308
|
+
continue;
|
|
309
|
+
updateSessionState(sessionId, root, (current) => {
|
|
310
|
+
if (!current)
|
|
311
|
+
return null;
|
|
312
|
+
const next = stripRegisterReceiptEpisodes(current, known);
|
|
313
|
+
return next === current ? null : next;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
for (const op of operations) {
|
|
289
317
|
if (op.delivery !== "acked" ||
|
|
290
318
|
!op.receiptId ||
|
|
291
319
|
op.kind !== "register" ||
|
|
@@ -15,7 +15,7 @@ import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
|
|
|
15
15
|
import { resolveProjectTask, shouldAskAfter } from "./project.js";
|
|
16
16
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
|
|
17
17
|
import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
|
|
18
|
-
import { mergeLocalSessionFields, readSessionState, writeSessionState, } from "./state.js";
|
|
18
|
+
import { mergeLocalSessionFields, isRegisterReceiptEpisodeId, readSessionState, stripRegisterReceiptEpisodes, writeSessionState, } from "./state.js";
|
|
19
19
|
function resultOf(parts) {
|
|
20
20
|
const out = {
|
|
21
21
|
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
@@ -174,7 +174,22 @@ export async function reconcileObservation(obs, deps) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
// Already untracked locally — stay untracked until explicit opt-in (no backfill).
|
|
177
|
-
const
|
|
177
|
+
const observedPrior = readSessionState(sessionId, deps.root);
|
|
178
|
+
// Acked register operations for this session. A register response never
|
|
179
|
+
// carries a binding episode, so an already acknowledged binding is
|
|
180
|
+
// identified by its company / project / task, never by comparing the
|
|
181
|
+
// register receipt with the session's bindingEpisodeId.
|
|
182
|
+
const ackedRegisters = observedPrior
|
|
183
|
+
? listOutboxOperations(deps.root).filter((operation) => operation.kind === "register" &&
|
|
184
|
+
operation.delivery === "acked" &&
|
|
185
|
+
operation.sessionId === sessionId &&
|
|
186
|
+
Boolean(operation.receiptId))
|
|
187
|
+
: [];
|
|
188
|
+
// Older clients stored a register receipt as the episode. Work from a
|
|
189
|
+
// projection with those cleared so no later write carries one forward.
|
|
190
|
+
const prior = observedPrior
|
|
191
|
+
? stripRegisterReceiptEpisodes(observedPrior, ackedRegisters.map((operation) => operation.receiptId))
|
|
192
|
+
: observedPrior;
|
|
178
193
|
if (prior?.contextStatus === "untracked" && prior.untrackedReceipt?.origin === "user") {
|
|
179
194
|
return {
|
|
180
195
|
result: resultOf({
|
|
@@ -690,15 +705,6 @@ export async function reconcileObservation(obs, deps) {
|
|
|
690
705
|
registerClassification = "needs_project";
|
|
691
706
|
diagnostics.push(`company rebind to ${resolvedCompany.slug ?? resolvedCompany.uid}; cleared project`);
|
|
692
707
|
}
|
|
693
|
-
// Acked register operations for this session. A register that did not come
|
|
694
|
-
// back bound yields no binding episode, so its receipt (not the session's
|
|
695
|
-
// bindingEpisodeId) is what identifies an already acknowledged binding.
|
|
696
|
-
const ackedRegisters = prior
|
|
697
|
-
? listOutboxOperations(deps.root).filter((operation) => operation.kind === "register" &&
|
|
698
|
-
operation.delivery === "acked" &&
|
|
699
|
-
operation.sessionId === sessionId &&
|
|
700
|
-
Boolean(operation.receiptId))
|
|
701
|
-
: [];
|
|
702
708
|
const stateClassification = registerClassification === "unresolved" &&
|
|
703
709
|
prior?.contextStatus === "needs_project" &&
|
|
704
710
|
prior.companyUid === resolvedCompany.uid &&
|
|
@@ -711,22 +717,26 @@ export async function reconcileObservation(obs, deps) {
|
|
|
711
717
|
// into a second semantic register operation: the prior receipt is the stable
|
|
712
718
|
// identity for this exact company/project/task binding.
|
|
713
719
|
const priorBindingIsBound = Boolean(projectId && taskId);
|
|
720
|
+
// A state that is not an acknowledged binding (for example one quarantined
|
|
721
|
+
// as company_conflict) must register again even when its fields still match.
|
|
722
|
+
const priorIsAcknowledgedBinding = prior?.contextStatus === "bound" ||
|
|
723
|
+
prior?.contextStatus === "needs_task" ||
|
|
724
|
+
prior?.contextStatus === "needs_project" ||
|
|
725
|
+
prior?.contextStatus === "unresolved";
|
|
714
726
|
const acknowledgedReceipt = prior &&
|
|
727
|
+
priorIsAcknowledgedBinding &&
|
|
715
728
|
prior.companyUid === resolvedCompany.uid &&
|
|
716
729
|
prior.companySlug === resolvedCompany.slug &&
|
|
717
730
|
prior.projectId === projectId &&
|
|
718
731
|
prior.taskId === taskId
|
|
719
|
-
? ackedRegisters.find((operation) =>
|
|
720
|
-
? operation.receiptId === prior.bindingEpisodeId
|
|
721
|
-
: !prior.bindingEpisodeId || operation.receiptId === prior.bindingEpisodeId) &&
|
|
722
|
-
operation.companyUid === resolvedCompany.uid &&
|
|
732
|
+
? ackedRegisters.find((operation) => operation.companyUid === resolvedCompany.uid &&
|
|
723
733
|
operation.companySlug === resolvedCompany.slug &&
|
|
724
734
|
operation.projectId === projectId &&
|
|
725
735
|
operation.taskId === taskId)?.receiptId
|
|
726
736
|
: undefined;
|
|
727
737
|
const acknowledgedPrior = acknowledgedReceipt ? prior : null;
|
|
728
738
|
if (acknowledgedPrior) {
|
|
729
|
-
//
|
|
739
|
+
// Only a bound session holds a server episode (issued by bind / organize).
|
|
730
740
|
const retainedEpisodeId = priorBindingIsBound
|
|
731
741
|
? acknowledgedPrior.bindingEpisodeId
|
|
732
742
|
: undefined;
|
|
@@ -895,10 +905,9 @@ export async function reconcileObservation(obs, deps) {
|
|
|
895
905
|
// the true computed classification into durable session state; otherwise
|
|
896
906
|
// the emitter and mesh presence lose the tenant the server already
|
|
897
907
|
// acknowledged. projectId / taskId are written only when genuinely
|
|
898
|
-
// resolved
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
// binding; the default merge preserves that when the binding still matches.)
|
|
908
|
+
// resolved. A register ack never yields a bindingEpisodeId, bound or not:
|
|
909
|
+
// the server issues episodes only on bind / organize. The default merge
|
|
910
|
+
// keeps an episode the session already holds when the binding still matches.
|
|
902
911
|
if (resolvedCompany.uid) {
|
|
903
912
|
const bound = registerClassification === "bound";
|
|
904
913
|
// A server ack of the company makes it authoritative: the durable status
|
|
@@ -919,7 +928,6 @@ export async function reconcileObservation(obs, deps) {
|
|
|
919
928
|
contextStatus: bound ? "bound" : ackedClassification,
|
|
920
929
|
...(projectId ? { projectId } : {}),
|
|
921
930
|
...(taskId ? { taskId } : {}),
|
|
922
|
-
...(bound ? { bindingEpisodeId: delivered.receiptId } : {}),
|
|
923
931
|
updatedAt: nowFn().toISOString(),
|
|
924
932
|
startedAt,
|
|
925
933
|
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
@@ -939,6 +947,12 @@ export async function reconcileObservation(obs, deps) {
|
|
|
939
947
|
: registerClassification === "needs_project"
|
|
940
948
|
? "needs_project"
|
|
941
949
|
: "queued";
|
|
950
|
+
// Report the episode the session actually holds, never the receipt.
|
|
951
|
+
const ackedState = readSessionState(sessionId, deps.root);
|
|
952
|
+
const ackedEpisodeId = ackedState &&
|
|
953
|
+
!isRegisterReceiptEpisodeId(ackedState.bindingEpisodeId, [delivered.receiptId])
|
|
954
|
+
? ackedState.bindingEpisodeId
|
|
955
|
+
: undefined;
|
|
942
956
|
return {
|
|
943
957
|
result: resultOf({
|
|
944
958
|
kind: kind === "queued" ? "queued" : kind,
|
|
@@ -950,7 +964,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
950
964
|
companySlug: resolvedCompany.slug,
|
|
951
965
|
projectId,
|
|
952
966
|
taskId,
|
|
953
|
-
bindingEpisodeId:
|
|
967
|
+
...(ackedEpisodeId ? { bindingEpisodeId: ackedEpisodeId } : {}),
|
|
954
968
|
...bindResultFields,
|
|
955
969
|
}),
|
|
956
970
|
exitCode: EXIT_OK,
|
|
@@ -158,6 +158,22 @@ export declare function mergeLocalSessionFields(next: SessionStateFile, prior: S
|
|
|
158
158
|
preserveBindingEpisode?: boolean;
|
|
159
159
|
preserveCompanyBinding?: boolean;
|
|
160
160
|
}): SessionStateFile;
|
|
161
|
+
/**
|
|
162
|
+
* True when a stored bindingEpisodeId is a register receipt, not an episode.
|
|
163
|
+
*
|
|
164
|
+
* The server mints binding episodes as UUIDs, and only on bind / organize. A
|
|
165
|
+
* register response carries no episode: its receipt is the operation id the
|
|
166
|
+
* client generated (`op_...`). An id is treated as a receipt when it has that
|
|
167
|
+
* shape or when it equals a receipt the caller knows about. A UUID is never
|
|
168
|
+
* treated as a receipt by shape.
|
|
169
|
+
*/
|
|
170
|
+
export declare function isRegisterReceiptEpisodeId(id: string | undefined, knownReceiptIds?: Iterable<string | undefined>): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Clear register receipts stored as a binding episode, on the session itself
|
|
173
|
+
* and on its pending presenceLeave. Returns the same object when nothing
|
|
174
|
+
* changed, so callers can detect a repair by identity.
|
|
175
|
+
*/
|
|
176
|
+
export declare function stripRegisterReceiptEpisodes(state: SessionStateFile, knownReceiptIds?: Iterable<string | undefined>): SessionStateFile;
|
|
161
177
|
/**
|
|
162
178
|
* Upgrade the local projection after the server acknowledges a register.
|
|
163
179
|
*
|
|
@@ -165,6 +181,9 @@ export declare function mergeLocalSessionFields(next: SessionStateFile, prior: S
|
|
|
165
181
|
* erase an explicit untracked choice, a conflicting existing company, or a
|
|
166
182
|
* more specific binding for the same company. This is also used to repair
|
|
167
183
|
* state files created by older clients before this write-back existed.
|
|
184
|
+
*
|
|
185
|
+
* The read and the write happen under the session state lock, so a concurrent
|
|
186
|
+
* writer (for example bind-project) is never overwritten from a stale snapshot.
|
|
168
187
|
*/
|
|
169
188
|
export declare function writeAcknowledgedRegisterBinding(binding: AcknowledgedRegisterBinding, root: string, now?: () => Date): SessionStateFile | null;
|
|
170
189
|
/** True when a durable state file was written by the hook / reconcile path. */
|
|
@@ -342,6 +342,47 @@ function bindingStrength(state) {
|
|
|
342
342
|
return 1;
|
|
343
343
|
return 0;
|
|
344
344
|
}
|
|
345
|
+
const SERVER_EPISODE_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
346
|
+
/**
|
|
347
|
+
* True when a stored bindingEpisodeId is a register receipt, not an episode.
|
|
348
|
+
*
|
|
349
|
+
* The server mints binding episodes as UUIDs, and only on bind / organize. A
|
|
350
|
+
* register response carries no episode: its receipt is the operation id the
|
|
351
|
+
* client generated (`op_...`). An id is treated as a receipt when it has that
|
|
352
|
+
* shape or when it equals a receipt the caller knows about. A UUID is never
|
|
353
|
+
* treated as a receipt by shape.
|
|
354
|
+
*/
|
|
355
|
+
export function isRegisterReceiptEpisodeId(id, knownReceiptIds = []) {
|
|
356
|
+
if (!id)
|
|
357
|
+
return false;
|
|
358
|
+
for (const receiptId of knownReceiptIds) {
|
|
359
|
+
if (receiptId && receiptId === id)
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (SERVER_EPISODE_SHAPE.test(id))
|
|
363
|
+
return false;
|
|
364
|
+
return id.startsWith("op_");
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Clear register receipts stored as a binding episode, on the session itself
|
|
368
|
+
* and on its pending presenceLeave. Returns the same object when nothing
|
|
369
|
+
* changed, so callers can detect a repair by identity.
|
|
370
|
+
*/
|
|
371
|
+
export function stripRegisterReceiptEpisodes(state, knownReceiptIds = []) {
|
|
372
|
+
const known = [...knownReceiptIds];
|
|
373
|
+
const ownIsReceipt = isRegisterReceiptEpisodeId(state.bindingEpisodeId, known);
|
|
374
|
+
const leaveIsReceipt = isRegisterReceiptEpisodeId(state.presenceLeave?.bindingEpisodeId, known);
|
|
375
|
+
if (!ownIsReceipt && !leaveIsReceipt)
|
|
376
|
+
return state;
|
|
377
|
+
const out = { ...state };
|
|
378
|
+
if (ownIsReceipt)
|
|
379
|
+
out.bindingEpisodeId = undefined;
|
|
380
|
+
if (leaveIsReceipt && state.presenceLeave) {
|
|
381
|
+
const { bindingEpisodeId: _receipt, ...leave } = state.presenceLeave;
|
|
382
|
+
out.presenceLeave = leave;
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
345
386
|
/**
|
|
346
387
|
* Upgrade the local projection after the server acknowledges a register.
|
|
347
388
|
*
|
|
@@ -349,60 +390,68 @@ function bindingStrength(state) {
|
|
|
349
390
|
* erase an explicit untracked choice, a conflicting existing company, or a
|
|
350
391
|
* more specific binding for the same company. This is also used to repair
|
|
351
392
|
* state files created by older clients before this write-back existed.
|
|
393
|
+
*
|
|
394
|
+
* The read and the write happen under the session state lock, so a concurrent
|
|
395
|
+
* writer (for example bind-project) is never overwritten from a stale snapshot.
|
|
352
396
|
*/
|
|
353
397
|
export function writeAcknowledgedRegisterBinding(binding, root, now = () => new Date()) {
|
|
354
398
|
const acknowledged = acknowledgedStatus(binding);
|
|
355
399
|
if (!acknowledged || !isSafeWorkContextSegment(binding.sessionId))
|
|
356
400
|
return null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
prior
|
|
398
|
-
prior.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
401
|
+
let result = null;
|
|
402
|
+
updateSessionState(binding.sessionId, root, (current) => {
|
|
403
|
+
// Reconcile always writes the local projection before network delivery. Do
|
|
404
|
+
// not manufacture session files when replay sees an orphaned legacy outbox
|
|
405
|
+
// record with no corresponding tracked session.
|
|
406
|
+
if (!current)
|
|
407
|
+
return null;
|
|
408
|
+
// A register response never carries a server episode, bound or not. The
|
|
409
|
+
// server mints episodes only on bind / organize, and drops session-event
|
|
410
|
+
// batches whose episode it does not recognise. Older clients stored the
|
|
411
|
+
// register receipt as the episode, so clear it wherever it is found. This
|
|
412
|
+
// repair is independent of the binding decision below and therefore also
|
|
413
|
+
// applies to untracked and different-company states.
|
|
414
|
+
const prior = stripRegisterReceiptEpisodes(current, [binding.receiptId]);
|
|
415
|
+
const repaired = prior !== current;
|
|
416
|
+
if (prior.contextStatus === "untracked" ||
|
|
417
|
+
(prior.companyUid && prior.companyUid !== binding.companyUid)) {
|
|
418
|
+
result = prior;
|
|
419
|
+
return repaired ? prior : null;
|
|
420
|
+
}
|
|
421
|
+
const acknowledgementStrength = acknowledged === "bound" ? 3 : acknowledged === "needs_task" ? 2 : 1;
|
|
422
|
+
const priorStrength = bindingStrength(prior);
|
|
423
|
+
const preservePriorBinding = Boolean(prior.companyUid === binding.companyUid && priorStrength > acknowledgementStrength);
|
|
424
|
+
const sameBindingAsPrior = prior.companyUid === binding.companyUid &&
|
|
425
|
+
prior.projectId === binding.projectId &&
|
|
426
|
+
prior.taskId === binding.taskId;
|
|
427
|
+
const next = mergeLocalSessionFields({
|
|
428
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
429
|
+
sessionId: binding.sessionId,
|
|
430
|
+
companyUid: binding.companyUid,
|
|
431
|
+
companySlug: binding.companySlug ?? prior.companySlug,
|
|
432
|
+
contextStatus: preservePriorBinding ? prior.contextStatus : acknowledged,
|
|
433
|
+
projectId: preservePriorBinding ? prior.projectId : binding.projectId,
|
|
434
|
+
taskId: preservePriorBinding ? prior.taskId : binding.taskId,
|
|
435
|
+
// Keep a server-issued episode only while the binding it was issued
|
|
436
|
+
// for is unchanged. Never derive one from the receipt.
|
|
437
|
+
bindingEpisodeId: preservePriorBinding || sameBindingAsPrior
|
|
438
|
+
? prior.bindingEpisodeId
|
|
439
|
+
: undefined,
|
|
440
|
+
updatedAt: now().toISOString(),
|
|
441
|
+
}, prior, { preserveBindingEpisode: false });
|
|
442
|
+
if (prior.companyUid === next.companyUid &&
|
|
443
|
+
prior.companySlug === next.companySlug &&
|
|
444
|
+
prior.contextStatus === next.contextStatus &&
|
|
445
|
+
prior.projectId === next.projectId &&
|
|
446
|
+
prior.taskId === next.taskId &&
|
|
447
|
+
prior.bindingEpisodeId === next.bindingEpisodeId) {
|
|
448
|
+
result = prior;
|
|
449
|
+
return repaired ? prior : null;
|
|
450
|
+
}
|
|
451
|
+
result = next;
|
|
452
|
+
return next;
|
|
453
|
+
});
|
|
454
|
+
return result;
|
|
406
455
|
}
|
|
407
456
|
/** True when a durable state file was written by the hook / reconcile path. */
|
|
408
457
|
export function isHookWrittenSessionState(state) {
|