@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
@@ -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 acknowledgedPrior = prior?.bindingEpisodeId &&
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
- listOutboxOperations(deps.root).some((operation) => operation.kind === "register" &&
709
- operation.delivery === "acked" &&
710
- operation.sessionId === sessionId &&
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
- ? prior
717
- : null;
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: acknowledgedPrior.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=${acknowledgedPrior.bindingEpisodeId}`);
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: acknowledgedPrior.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
- ? prior.bindingEpisodeId ?? binding.receiptId
380
- : binding.receiptId,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.244.0",
3
+ "version": "5.245.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {