@indigoai-us/hq-cli 5.244.0 → 5.245.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,8 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.245.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Work Mesh sessions registered without a bound project (unresolved,
|
|
10
|
+
needs_project, needs_task) no longer store the register receipt as their
|
|
11
|
+
binding episode, and `hq mesh context bind-project` clears it. The server
|
|
12
|
+
dropped every session-event batch carrying that id, so turn and session ends
|
|
13
|
+
never landed and no work report posted. Existing state files are repaired on
|
|
14
|
+
the next flush.
|
|
15
|
+
|
|
5
16
|
## [5.244.0] — 2026-09-27
|
|
6
17
|
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A bot whose agent home is on a network share can now mint a token. The
|
|
21
|
+
refresh lock was a POSIX hard-link lock, and CIFS/SMB shares — Azure Files
|
|
22
|
+
again — do not implement `link()` at all, so every mint failed with
|
|
23
|
+
ENOTSUP and took the whole probe down with it: no heartbeat ever posted
|
|
24
|
+
and the inbox poller retried and failed every 15 seconds. The lock now
|
|
25
|
+
falls back to an exclusive create, which gives the same guarantee and
|
|
26
|
+
works there. Contention still surfaces as EEXIST on both paths, so
|
|
27
|
+
stale-owner reclamation is unchanged, and a link that fails for any other
|
|
28
|
+
reason is still a real error.
|
|
29
|
+
|
|
7
30
|
### Changed
|
|
8
31
|
|
|
9
32
|
- The setup bot no longer posts "This one takes a minute. I'll reply here as
|
|
@@ -1643,16 +1666,6 @@
|
|
|
1643
1666
|
|
|
1644
1667
|
### Fixed
|
|
1645
1668
|
|
|
1646
|
-
- A bot whose agent home is on a network share can now mint a token. The
|
|
1647
|
-
refresh lock was a POSIX hard-link lock, and CIFS/SMB shares — Azure Files
|
|
1648
|
-
again — do not implement `link()` at all, so every mint failed with
|
|
1649
|
-
ENOTSUP and took the whole probe down with it: no heartbeat ever posted
|
|
1650
|
-
and the inbox poller retried and failed every 15 seconds. The lock now
|
|
1651
|
-
falls back to an exclusive create, which gives the same guarantee and
|
|
1652
|
-
works there. Contention still surfaces as EEXIST on both paths, so
|
|
1653
|
-
stale-owner reclamation is unchanged, and a link that fails for any other
|
|
1654
|
-
reason is still a real error.
|
|
1655
|
-
|
|
1656
1669
|
- `hq mesh daemon install` and `hq mesh daemon run` unload and remove the
|
|
1657
1670
|
legacy `ai.getindigo.hq-work-mesh-listen` unit (LaunchAgent, systemd user
|
|
1658
1671
|
unit, or Scheduled Task) so only the daemon subscribes. The daemon writes
|
package/dist/commands/mesh.js
CHANGED
|
@@ -42,13 +42,12 @@ import { defaultRunCommand } from "../lib/service-manager/launchd.js";
|
|
|
42
42
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
43
43
|
import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
|
|
44
44
|
import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
|
|
45
|
-
import { readSessionState, updateSessionState } from "../lib/work-context/state.js";
|
|
45
|
+
import { applyOperatorProjectBinding, readSessionState, updateSessionState, } from "../lib/work-context/state.js";
|
|
46
46
|
import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind-best.js";
|
|
47
47
|
import { bindCallingSession, formatSessionBoundLine } from "../lib/work-context/bind-calling-session.js";
|
|
48
48
|
import { rebindNeedsProjectOnPrompt } from "../lib/work-context/prompt-rebind.js";
|
|
49
49
|
import { planPrdSync, rebindSessionToProject } from "../lib/work-context/prd-sync.js";
|
|
50
50
|
import { reserveSessionSeq } from "../lib/mesh/live/session-seq.js";
|
|
51
|
-
import { WORK_CONTEXT_CONTRACT_VERSION } from "../lib/work-context/contract.js";
|
|
52
51
|
import { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "../lib/work-context/reconcile.js";
|
|
53
52
|
import { resolveCompany } from "../lib/work-context/company.js";
|
|
54
53
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../lib/work-context/repo-remote.js";
|
|
@@ -835,20 +834,7 @@ async function runBindProject(target, opts) {
|
|
|
835
834
|
fail(`No candidate ${projectId} on session ${sessionId}.`);
|
|
836
835
|
projectId = chosen.projectId;
|
|
837
836
|
}
|
|
838
|
-
const updated = updateSessionState(sessionId, root, (state) =>
|
|
839
|
-
if (!state)
|
|
840
|
-
return null;
|
|
841
|
-
return {
|
|
842
|
-
...state,
|
|
843
|
-
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
844
|
-
contextStatus: state.taskId ? "bound" : "needs_task",
|
|
845
|
-
projectId,
|
|
846
|
-
projectGate: undefined,
|
|
847
|
-
projectCandidates: undefined,
|
|
848
|
-
projectBindReason: `operator selected ${projectId}`,
|
|
849
|
-
updatedAt: new Date().toISOString(),
|
|
850
|
-
};
|
|
851
|
-
});
|
|
837
|
+
const updated = updateSessionState(sessionId, root, (state) => state ? applyOperatorProjectBinding(state, projectId) : null);
|
|
852
838
|
if (!updated)
|
|
853
839
|
fail(`No work-context state for session ${sessionId}.`);
|
|
854
840
|
if (opts.json) {
|
|
@@ -690,32 +690,46 @@ export async function reconcileObservation(obs, deps) {
|
|
|
690
690
|
registerClassification = "needs_project";
|
|
691
691
|
diagnostics.push(`company rebind to ${resolvedCompany.slug ?? resolvedCompany.uid}; cleared project`);
|
|
692
692
|
}
|
|
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
|
+
: [];
|
|
693
702
|
const stateClassification = registerClassification === "unresolved" &&
|
|
694
703
|
prior?.contextStatus === "needs_project" &&
|
|
695
704
|
prior.companyUid === resolvedCompany.uid &&
|
|
696
|
-
prior.bindingEpisodeId
|
|
705
|
+
(prior.bindingEpisodeId ||
|
|
706
|
+
ackedRegisters.some((operation) => operation.companyUid === resolvedCompany.uid))
|
|
697
707
|
? "needs_project"
|
|
698
708
|
: registerClassification;
|
|
699
709
|
// Re-observing an already acknowledged binding is normal for hooks and the
|
|
700
710
|
// resident daemon. A fresh clientOperationId must not turn that observation
|
|
701
711
|
// into a second semantic register operation: the prior receipt is the stable
|
|
702
712
|
// identity for this exact company/project/task binding.
|
|
703
|
-
const
|
|
713
|
+
const priorBindingIsBound = Boolean(projectId && taskId);
|
|
714
|
+
const acknowledgedReceipt = prior &&
|
|
704
715
|
prior.companyUid === resolvedCompany.uid &&
|
|
705
716
|
prior.companySlug === resolvedCompany.slug &&
|
|
706
717
|
prior.projectId === projectId &&
|
|
707
|
-
prior.taskId === taskId
|
|
708
|
-
|
|
709
|
-
operation.
|
|
710
|
-
operation.
|
|
711
|
-
operation.receiptId === prior.bindingEpisodeId &&
|
|
718
|
+
prior.taskId === taskId
|
|
719
|
+
? ackedRegisters.find((operation) => (priorBindingIsBound
|
|
720
|
+
? operation.receiptId === prior.bindingEpisodeId
|
|
721
|
+
: !prior.bindingEpisodeId || operation.receiptId === prior.bindingEpisodeId) &&
|
|
712
722
|
operation.companyUid === resolvedCompany.uid &&
|
|
713
723
|
operation.companySlug === resolvedCompany.slug &&
|
|
714
724
|
operation.projectId === projectId &&
|
|
715
|
-
operation.taskId === taskId)
|
|
716
|
-
|
|
717
|
-
|
|
725
|
+
operation.taskId === taskId)?.receiptId
|
|
726
|
+
: undefined;
|
|
727
|
+
const acknowledgedPrior = acknowledgedReceipt ? prior : null;
|
|
718
728
|
if (acknowledgedPrior) {
|
|
729
|
+
// A partial (not bound) acknowledged register carries no server episode.
|
|
730
|
+
const retainedEpisodeId = priorBindingIsBound
|
|
731
|
+
? acknowledgedPrior.bindingEpisodeId
|
|
732
|
+
: undefined;
|
|
719
733
|
// Activity is local progress, not binding identity. Retain the receipt
|
|
720
734
|
// without another register while carrying hook counters forward so the
|
|
721
735
|
// normal clarification threshold still advances.
|
|
@@ -727,7 +741,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
727
741
|
contextStatus: stateClassification,
|
|
728
742
|
projectId,
|
|
729
743
|
taskId,
|
|
730
|
-
bindingEpisodeId:
|
|
744
|
+
bindingEpisodeId: retainedEpisodeId,
|
|
731
745
|
updatedAt: nowIso,
|
|
732
746
|
startedAt,
|
|
733
747
|
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
@@ -735,7 +749,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
735
749
|
...(decision ? { decision } : {}),
|
|
736
750
|
...bindStateFields,
|
|
737
751
|
...transcriptFields,
|
|
738
|
-
}, prior), deps.root);
|
|
752
|
+
}, prior, { preserveBindingEpisode: priorBindingIsBound }), deps.root);
|
|
739
753
|
const kind = registerClassification === "bound"
|
|
740
754
|
? "bound"
|
|
741
755
|
: registerClassification === "needs_task"
|
|
@@ -743,7 +757,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
743
757
|
: registerClassification === "needs_project"
|
|
744
758
|
? "needs_project"
|
|
745
759
|
: "queued";
|
|
746
|
-
diagnostics.push(`binding unchanged; retained receipt=${
|
|
760
|
+
diagnostics.push(`binding unchanged; retained receipt=${acknowledgedReceipt}`);
|
|
747
761
|
return {
|
|
748
762
|
result: resultOf({
|
|
749
763
|
kind,
|
|
@@ -755,7 +769,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
755
769
|
companySlug: resolvedCompany.slug,
|
|
756
770
|
projectId,
|
|
757
771
|
taskId,
|
|
758
|
-
bindingEpisodeId:
|
|
772
|
+
...(retainedEpisodeId ? { bindingEpisodeId: retainedEpisodeId } : {}),
|
|
759
773
|
...bindResultFields,
|
|
760
774
|
}),
|
|
761
775
|
exitCode: EXIT_OK,
|
|
@@ -174,4 +174,12 @@ export declare function authoritativeCompanyFromState(state: SessionStateFile |
|
|
|
174
174
|
slug?: string;
|
|
175
175
|
uid?: string;
|
|
176
176
|
} | null;
|
|
177
|
+
/**
|
|
178
|
+
* Apply `hq mesh context bind-project`: an operator's local project choice.
|
|
179
|
+
*
|
|
180
|
+
* The server issues no binding episode for this local choice, so any prior
|
|
181
|
+
* bindingEpisodeId (older clients stored a register receipt there) is cleared.
|
|
182
|
+
* Carrying it forward makes the server drop every later session-event batch.
|
|
183
|
+
*/
|
|
184
|
+
export declare function applyOperatorProjectBinding(state: SessionStateFile, projectId: string, now?: () => Date): SessionStateFile;
|
|
177
185
|
//# sourceMappingURL=state.d.ts.map
|
|
@@ -367,6 +367,19 @@ export function writeAcknowledgedRegisterBinding(binding, root, now = () => new
|
|
|
367
367
|
const acknowledgementStrength = acknowledged === "bound" ? 3 : acknowledged === "needs_task" ? 2 : 1;
|
|
368
368
|
const priorStrength = bindingStrength(prior);
|
|
369
369
|
const preservePriorBinding = Boolean(prior.companyUid === binding.companyUid && priorStrength > acknowledgementStrength);
|
|
370
|
+
// The server issues no binding episode for a register that did not come
|
|
371
|
+
// back bound (unresolved / needs_project / needs_task). Its receipt must
|
|
372
|
+
// never stand in for one: session-event batches carry bindingEpisodeId and
|
|
373
|
+
// the server drops batches whose episode it does not recognise. Older
|
|
374
|
+
// clients stored that receipt here, so also repair a prior value that is
|
|
375
|
+
// exactly this partial register's receipt.
|
|
376
|
+
const receiptEpisode = acknowledged === "bound" ? binding.receiptId : undefined;
|
|
377
|
+
const priorEpisode = acknowledged !== "bound" && prior.bindingEpisodeId === binding.receiptId
|
|
378
|
+
? undefined
|
|
379
|
+
: prior.bindingEpisodeId;
|
|
380
|
+
const sameBindingAsPrior = prior.companyUid === binding.companyUid &&
|
|
381
|
+
prior.projectId === binding.projectId &&
|
|
382
|
+
prior.taskId === binding.taskId;
|
|
370
383
|
const next = mergeLocalSessionFields({
|
|
371
384
|
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
372
385
|
sessionId: binding.sessionId,
|
|
@@ -376,10 +389,10 @@ export function writeAcknowledgedRegisterBinding(binding, root, now = () => new
|
|
|
376
389
|
projectId: preservePriorBinding ? prior.projectId : binding.projectId,
|
|
377
390
|
taskId: preservePriorBinding ? prior.taskId : binding.taskId,
|
|
378
391
|
bindingEpisodeId: preservePriorBinding
|
|
379
|
-
?
|
|
380
|
-
:
|
|
392
|
+
? priorEpisode ?? receiptEpisode
|
|
393
|
+
: receiptEpisode ?? (sameBindingAsPrior ? priorEpisode : undefined),
|
|
381
394
|
updatedAt: now().toISOString(),
|
|
382
|
-
}, prior);
|
|
395
|
+
}, prior, { preserveBindingEpisode: false });
|
|
383
396
|
if (prior.companyUid === next.companyUid &&
|
|
384
397
|
prior.companySlug === next.companySlug &&
|
|
385
398
|
prior.contextStatus === next.contextStatus &&
|
|
@@ -418,4 +431,24 @@ export function authoritativeCompanyFromState(state) {
|
|
|
418
431
|
return null;
|
|
419
432
|
return { slug: state.companySlug, uid: state.companyUid };
|
|
420
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Apply `hq mesh context bind-project`: an operator's local project choice.
|
|
436
|
+
*
|
|
437
|
+
* The server issues no binding episode for this local choice, so any prior
|
|
438
|
+
* bindingEpisodeId (older clients stored a register receipt there) is cleared.
|
|
439
|
+
* Carrying it forward makes the server drop every later session-event batch.
|
|
440
|
+
*/
|
|
441
|
+
export function applyOperatorProjectBinding(state, projectId, now = () => new Date()) {
|
|
442
|
+
return {
|
|
443
|
+
...state,
|
|
444
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
445
|
+
contextStatus: state.taskId ? "bound" : "needs_task",
|
|
446
|
+
projectId,
|
|
447
|
+
bindingEpisodeId: undefined,
|
|
448
|
+
projectGate: undefined,
|
|
449
|
+
projectCandidates: undefined,
|
|
450
|
+
projectBindReason: `operator selected ${projectId}`,
|
|
451
|
+
updatedAt: now().toISOString(),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
421
454
|
//# sourceMappingURL=state.js.map
|