@indigoai-us/hq-cli 5.312.2-beta.0 → 5.313.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
  3. package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
  4. package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
  5. package/assets/scaffold/core/scripts/register-project.sh +262 -0
  6. package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
  7. package/dist/command-catalog.generated.d.ts +80 -0
  8. package/dist/command-catalog.generated.js +99 -0
  9. package/dist/commands/cloud.js +0 -1
  10. package/dist/commands/company-rename-flag.d.ts +4 -0
  11. package/dist/commands/company-rename-flag.js +7 -0
  12. package/dist/commands/company-rename.d.ts +4 -0
  13. package/dist/commands/company-rename.js +90 -0
  14. package/dist/commands/company.js +2 -0
  15. package/dist/commands/core-eval-trigger-native.d.ts +4 -0
  16. package/dist/commands/core-eval-trigger-native.js +12 -0
  17. package/dist/commands/core-knowledge-prefs.d.ts +0 -1
  18. package/dist/commands/core-knowledge-prefs.js +1 -1
  19. package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
  20. package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
  21. package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
  22. package/dist/commands/core-native-hook-helper-commands.js +56 -0
  23. package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
  24. package/dist/commands/core-native-hook-helpers-flag.js +7 -0
  25. package/dist/commands/core-register-project-native.d.ts +30 -0
  26. package/dist/commands/core-register-project-native.js +347 -0
  27. package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
  28. package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
  29. package/dist/commands/core-worker-share-native.d.ts +0 -1
  30. package/dist/commands/core-worker-share-native.js +1 -1
  31. package/dist/commands/core.js +7 -0
  32. package/dist/commands/lanes-message.js +3 -0
  33. package/dist/commands/lanes-questions.js +2 -1
  34. package/dist/commands/lanes-resume.js +3 -0
  35. package/dist/commands/lanes.js +151 -21
  36. package/dist/fast-core.js +10 -3
  37. package/dist/fast-scaffold.d.ts +2 -0
  38. package/dist/fast-scaffold.js +11 -0
  39. package/dist/lib/doctor/checks/work-context.js +9 -1
  40. package/dist/lib/lanes/config.d.ts +3 -0
  41. package/dist/lib/lanes/config.js +14 -0
  42. package/dist/lib/lanes/disk-guard.d.ts +66 -0
  43. package/dist/lib/lanes/disk-guard.js +172 -0
  44. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  45. package/dist/lib/lanes/flag-keys.js +2 -0
  46. package/dist/lib/lanes/grants.d.ts +4 -3
  47. package/dist/lib/lanes/grants.js +12 -7
  48. package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
  49. package/dist/lib/lanes/index.d.ts +1 -1
  50. package/dist/lib/lanes/index.js +1 -1
  51. package/dist/lib/lanes/reconcile.d.ts +7 -1
  52. package/dist/lib/lanes/reconcile.js +142 -11
  53. package/dist/lib/lanes/registry-flag.d.ts +6 -0
  54. package/dist/lib/lanes/registry-flag.js +25 -0
  55. package/dist/lib/lanes/spawn.d.ts +5 -0
  56. package/dist/lib/lanes/spawn.js +26 -1
  57. package/dist/lib/lanes/store.js +11 -0
  58. package/dist/lib/lanes/types.d.ts +11 -1
  59. package/dist/lib/lanes/types.js +1 -0
  60. package/dist/lib/mesh/client.js +23 -5
  61. package/dist/lib/mesh/denial-codes.d.ts +5 -0
  62. package/dist/lib/mesh/denial-codes.js +14 -0
  63. package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
  64. package/dist/lib/mesh/live/daemon/doctor.js +11 -1
  65. package/dist/lib/search-index/embed-lock.d.ts +2 -0
  66. package/dist/lib/search-index/embed-lock.js +10 -1
  67. package/dist/lib/work-context/contract.d.ts +3 -0
  68. package/dist/lib/work-context/contract.schema.json +3 -1
  69. package/dist/lib/work-context/outbox.d.ts +14 -5
  70. package/dist/lib/work-context/outbox.js +48 -5
  71. package/dist/lib/work-context/reconcile.js +26 -5
  72. package/dist/main.d.ts +20 -0
  73. package/dist/main.js +8 -1
  74. package/dist/utils/core-eval-trigger.d.ts +4 -0
  75. package/dist/utils/core-eval-trigger.js +88 -0
  76. package/dist/utils/pack-contributions.d.ts +0 -5
  77. package/dist/utils/pack-contributions.js +1 -1
  78. package/dist/utils/redact-error-text.d.ts +0 -2
  79. package/dist/utils/redact-error-text.js +1 -1
  80. package/dist/utils/sandbox-runner-client.d.ts +2 -1
  81. package/package.json +1 -1
@@ -14,6 +14,7 @@ import { type KnownModelsDeps } from "./models.js";
14
14
  import { type LaunchTrigger } from "./delivery.js";
15
15
  import { type MachineIdentity } from "./machine-identity.js";
16
16
  import { type AdmissionHost } from "./admission.js";
17
+ import { type StatFs } from "./disk-guard.js";
17
18
  import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHistoryEntry } from "./types.js";
18
19
  export declare const LANES_RUNS_REL: string;
19
20
  /** Written by reconcile before a resume spawn; the worker prompt names this path. */
@@ -74,6 +75,10 @@ export interface SpawnLaneDeps {
74
75
  hostname?: string;
75
76
  /** The caller's host scope for admission checks; reconcile supplies its resolved host. */
76
77
  admissionHost?: AdmissionHost;
78
+ /** Resolved per-company by the async command/reconcile boundary. */
79
+ lowDiskGuardEnabled?: boolean;
80
+ /** Injected statfs boundary; production uses node:fs.statfsSync. */
81
+ statfs?: StatFs;
77
82
  /** Provider model catalog dependencies, shared with create in tests. */
78
83
  modelDeps?: KnownModelsDeps;
79
84
  /** Create validated this exact account/model candidate before its admission lock. */
@@ -36,6 +36,7 @@ import { MESSAGE_REPLAY_STATE_NAME } from "./message-replay.js";
36
36
  import { markAnsweredQuestionsDelivered } from "./questions.js";
37
37
  import { hasEarlierEligibleQueuedLane, laneAdmissionCapReason, withLaneAdmissionLock, } from "./admission.js";
38
38
  import { resolveAdmissionHost } from "./admission-host.js";
39
+ import { checkLaneDiskSpace, lowDiskGuardEnabledForLane } from "./disk-guard.js";
39
40
  import { LANE_ENVELOPE_JSON_SCHEMA, ENVELOPE_INVALID_REASON, parseLaneProvider, } from "./types.js";
40
41
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
41
42
  /** Written by reconcile before a resume spawn; the worker prompt names this path. */
@@ -539,7 +540,10 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
539
540
  });
540
541
  if (capacityReason !== undefined || earlierEligibleLane)
541
542
  return moved;
542
- return spawnLaneWorker(hqRoot, moved, deps);
543
+ return spawnLaneWorker(hqRoot, moved, {
544
+ ...deps,
545
+ lowDiskGuardEnabled: deps.lowDiskGuardEnabled ?? await lowDiskGuardEnabledForLane(hqRoot, moved),
546
+ });
543
547
  });
544
548
  }
545
549
  export function appendModelFlag(provider, args, model) {
@@ -1802,6 +1806,27 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1802
1806
  if (preservedAccountModel !== undefined)
1803
1807
  pinnedModel = preservedAccountModel;
1804
1808
  const resolved = selection.account;
1809
+ if (deps.lowDiskGuardEnabled === true) {
1810
+ const free = checkLaneDiskSpace(hqRoot, resolved.home, deps.statfs, config.low_disk_min_free_bytes);
1811
+ if (free.low) {
1812
+ const measuredAt = (deps.now ?? (() => new Date()))().toISOString();
1813
+ return retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
1814
+ ...current,
1815
+ state: "queued",
1816
+ queued_reason: "low_disk",
1817
+ queued_details: {
1818
+ reason: "low_disk",
1819
+ hq_free_bytes: free.hq.free_bytes,
1820
+ provider_free_bytes: free.provider.free_bytes,
1821
+ measured_at: measuredAt,
1822
+ },
1823
+ queued_at: current.queued_at ?? measuredAt,
1824
+ admission_reserved_at: undefined,
1825
+ terminal_reason: undefined,
1826
+ terminal_details: undefined,
1827
+ }));
1828
+ }
1829
+ }
1805
1830
  const status = accountStatus(lane.provider, resolved.id, config, { existsSync, hostname });
1806
1831
  if (isLoggedOut(status)) {
1807
1832
  const failed = markPreSpawnFailure
@@ -612,6 +612,17 @@ export function parseLaneRecord(raw, filePath) {
612
612
  });
613
613
  }
614
614
  }
615
+ else if (detailsRecord.reason === "low_disk") {
616
+ if (raw.state !== "queued" || raw.queued_reason !== "low_disk" ||
617
+ !Number.isSafeInteger(detailsRecord.hq_free_bytes) || detailsRecord.hq_free_bytes < 0 ||
618
+ !Number.isSafeInteger(detailsRecord.provider_free_bytes) || detailsRecord.provider_free_bytes < 0) {
619
+ throw new LaneSchemaError("queued_details must describe measured low-disk queue state", {
620
+ path: filePath,
621
+ field: "queued_details",
622
+ });
623
+ }
624
+ requireTimestamp(detailsRecord.measured_at, "queued_details.measured_at", filePath);
625
+ }
615
626
  else {
616
627
  throw new LaneSchemaError("queued_details must describe a supported queue condition", {
617
628
  path: filePath,
@@ -177,7 +177,7 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
177
177
  export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
178
178
  export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
179
179
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
180
- export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed"];
180
+ export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk"];
181
181
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
182
182
  export type QueuedDetails = {
183
183
  reason: "worker_profile_changed";
@@ -188,6 +188,11 @@ export type QueuedDetails = {
188
188
  reason: "outpost_dispatch_unknown";
189
189
  outcome_unknown: true;
190
190
  command_id: string | null;
191
+ } | {
192
+ reason: "low_disk";
193
+ hq_free_bytes: number;
194
+ provider_free_bytes: number;
195
+ measured_at: string;
191
196
  };
192
197
  export declare function isQueuedReason(value: unknown): value is QueuedReason;
193
198
  export declare const HOST_KINDS: readonly ["local", "outpost"];
@@ -1007,6 +1012,11 @@ export interface LanesConfig {
1007
1012
  checks: TrackerChecksConfig;
1008
1013
  };
1009
1014
  runaway_search_guard?: RunawaySearchGuardConfig;
1015
+ /** Optional free-space threshold override for the low-disk admission gate. */
1016
+ low_disk_min_free_bytes?: number;
1017
+ /** Retention ages for Codex session rollout maintenance. */
1018
+ codex_rollout_compress_after_hours?: number;
1019
+ codex_rollout_delete_after_days?: number;
1010
1020
  /** Seconds an unanswered question waits at a session audience before advancing. */
1011
1021
  session_question_timeout_secs: number;
1012
1022
  /** Age in hours after which an open manager blocker must be re-verified. */
@@ -112,6 +112,7 @@ export const QUEUED_REASONS = [
112
112
  "provider_binary_missing",
113
113
  "account_retry",
114
114
  "worker_profile_changed",
115
+ "low_disk",
115
116
  ];
116
117
  export function isQueuedReason(value) {
117
118
  return typeof value === "string" && QUEUED_REASONS.includes(value);
@@ -4,6 +4,7 @@
4
4
  * vault HTTP + receipt-shaped responses (US-007A / US-007B).
5
5
  */
6
6
  import { listActiveMembershipCompanies, meshJson, requireToken, resolveMeshCompany, } from "./api.js";
7
+ import { isWorkSessionDenialCode } from "./denial-codes.js";
7
8
  export { meshJson, requireToken, resolveMeshCompany, listActiveMembershipCompanies };
8
9
  export async function openMeshTransport(opts) {
9
10
  const token = await requireToken();
@@ -50,11 +51,28 @@ export function createWorkSessionDeliverer(opts) {
50
51
  const message = err instanceof Error ? err.message : String(err);
51
52
  // meshJson throws on !ok with error text; classify by leading status when present.
52
53
  const statusMatch = /^(\d{3})\b/.exec(message);
53
- if (statusMatch) {
54
- const status = Number(statusMatch[1]);
55
- if (status === 401 || status === 403) {
56
- return { ok: false, retryable: false, code: "AUTH_DENIED" };
57
- }
54
+ const errorFields = err && typeof err === "object"
55
+ ? err
56
+ : {};
57
+ const status = typeof errorFields.status === "number" &&
58
+ Number.isInteger(errorFields.status)
59
+ ? errorFields.status
60
+ : statusMatch
61
+ ? Number(statusMatch[1])
62
+ : undefined;
63
+ if (status === 401 || status === 403) {
64
+ const serverCode = isWorkSessionDenialCode(errorFields.code)
65
+ ? errorFields.code
66
+ : undefined;
67
+ return {
68
+ ok: false,
69
+ retryable: false,
70
+ code: "AUTH_DENIED",
71
+ httpStatus: status,
72
+ ...(serverCode ? { serverCode } : {}),
73
+ };
74
+ }
75
+ if (status !== undefined) {
58
76
  if (status === 400 || status === 422) {
59
77
  return { ok: false, retryable: false, code: "VALIDATION_FAILED" };
60
78
  }
@@ -0,0 +1,5 @@
1
+ /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
2
+ export declare const WORK_SESSION_DENIAL_CODES: readonly ["AUTH_REQUIRED", "AGENT_IDENTITY_UNVERIFIED", "IMPERSONATION_FORBIDDEN", "NO_PERSON_ENTITY", "INVALID_COMPANY", "WORK_REGISTRY_DISABLED"];
3
+ export type WorkSessionDenialCode = (typeof WORK_SESSION_DENIAL_CODES)[number];
4
+ export declare function isWorkSessionDenialCode(value: unknown): value is WorkSessionDenialCode;
5
+ //# sourceMappingURL=denial-codes.d.ts.map
@@ -0,0 +1,14 @@
1
+ /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
2
+ export const WORK_SESSION_DENIAL_CODES = [
3
+ "AUTH_REQUIRED",
4
+ "AGENT_IDENTITY_UNVERIFIED",
5
+ "IMPERSONATION_FORBIDDEN",
6
+ "NO_PERSON_ENTITY",
7
+ "INVALID_COMPANY",
8
+ "WORK_REGISTRY_DISABLED",
9
+ ];
10
+ export function isWorkSessionDenialCode(value) {
11
+ return typeof value === "string" &&
12
+ WORK_SESSION_DENIAL_CODES.includes(value);
13
+ }
14
+ //# sourceMappingURL=denial-codes.js.map
@@ -27,6 +27,8 @@ export interface DaemonDoctorReport {
27
27
  outboxDepth: number;
28
28
  outboxQuarantined?: number;
29
29
  outboxLastQuarantineReason?: string;
30
+ outboxLastQuarantineHttpStatus?: number;
31
+ outboxLastQuarantineServerCode?: string;
30
32
  lastFlushAt?: string;
31
33
  lastFlushResult?: DaemonStateFile["lastFlushResult"];
32
34
  unhealthy: boolean;
@@ -263,6 +263,8 @@ export function collectDaemonDoctor(deps = {}) {
263
263
  ? {
264
264
  outboxQuarantined: outbox.quarantined,
265
265
  outboxLastQuarantineReason: outbox.lastQuarantineReason ?? undefined,
266
+ outboxLastQuarantineHttpStatus: outbox.lastQuarantineHttpStatus ?? undefined,
267
+ outboxLastQuarantineServerCode: outbox.lastQuarantineServerCode ?? undefined,
266
268
  }
267
269
  : {}),
268
270
  lastFlushAt: state?.lastFlushAt,
@@ -310,7 +312,15 @@ export function formatDaemonDoctor(report) {
310
312
  lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}${report.heldCount > 0 ? " (needs_company; not sent as NONE)" : ""}`, `held with no local evidence: ${report.heldWithNoLocalEvidence}`, `outbox depth: ${report.outboxDepth}`);
311
313
  }
312
314
  if (report.outboxQuarantined !== undefined) {
313
- lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"})`);
315
+ const quarantineDetails = [
316
+ report.outboxLastQuarantineHttpStatus !== undefined
317
+ ? `HTTP ${report.outboxLastQuarantineHttpStatus}`
318
+ : "",
319
+ report.outboxLastQuarantineServerCode ?? "",
320
+ ]
321
+ .filter(Boolean)
322
+ .join(" ");
323
+ lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`);
314
324
  }
315
325
  if (report.emitMode !== "legacy" && report.heldCount > 0) {
316
326
  lines.push(`held: ${report.heldCount} (needs_company; not sent as NONE)`);
@@ -4,6 +4,8 @@ export type EmbedLockDependencies = {
4
4
  pid: number;
5
5
  random: () => string;
6
6
  isProcessAlive: (pid: number) => boolean;
7
+ /** Test seam for a contender arriving after mkdir and before owner publication. */
8
+ beforeOwnerPublish?: (ownerFile: string) => void;
7
9
  /** Test seam for simulating a competing owner replacing the atomic record. */
8
10
  afterOwnerPublish?: (ownerFile: string) => void;
9
11
  };
@@ -184,8 +184,13 @@ function createAndPublishLock(home, dependencies) {
184
184
  // parity: the script's write_owner_record honored the same variable.
185
185
  if (dependencies.env.QMD_FORCE_OWNER_WRITE_FAIL)
186
186
  throw new Error('forced owner write failure');
187
+ dependencies.beforeOwnerPublish?.(ownerFile);
187
188
  fs.writeFileSync(temporary, `pid=${dependencies.pid}\nts=${dependencies.now()}\nnonce=${nonce}\n`);
188
- fs.renameSync(temporary, ownerFile);
189
+ // renameSync replaces an existing owner on POSIX. A stale reclaim can
190
+ // replace this ownerless directory while this publisher is paused, so
191
+ // publish only if this exact generation still has no owner.
192
+ fs.linkSync(temporary, ownerFile);
193
+ fs.unlinkSync(temporary);
189
194
  dependencies.afterOwnerPublish?.(ownerFile);
190
195
  const verified = readOwner(directory);
191
196
  if (!verified || verified.pid !== dependencies.pid || verified.nonce !== nonce)
@@ -196,6 +201,10 @@ function createAndPublishLock(home, dependencies) {
196
201
  catch {
197
202
  // The marker identifies only the directory this process created. Do not
198
203
  // recursively remove the fixed lock path after a failed publication.
204
+ try {
205
+ fs.unlinkSync(temporary);
206
+ }
207
+ catch { /* absent after link or moved with a stale generation */ }
199
208
  fs.rmSync(marker, { recursive: true, force: true });
200
209
  removeDirectoryIfEmpty(directory);
201
210
  return false;
@@ -61,6 +61,9 @@ export interface ContextResult {
61
61
  bindingEpisodeId?: string;
62
62
  clientOperationId: string;
63
63
  errorCode?: string;
64
+ /** Sanitized HTTP denial details; raw response bodies are never included. */
65
+ httpStatus?: number;
66
+ serverCode?: string;
64
67
  /** Why a project was chosen, or why the session is asking. Local consumers only. */
65
68
  projectBindReason?: string;
66
69
  /** Ranked alternatives when classification is needs_project. */
@@ -100,7 +100,9 @@
100
100
  "taskId": { "type": "string", "maxLength": 128 },
101
101
  "bindingEpisodeId": { "type": "string", "maxLength": 128 },
102
102
  "clientOperationId": { "type": "string", "minLength": 1, "maxLength": 128 },
103
- "errorCode": { "type": "string", "maxLength": 64 }
103
+ "errorCode": { "type": "string", "maxLength": 64 },
104
+ "httpStatus": { "type": "number" },
105
+ "serverCode": { "type": "string", "maxLength": 64 }
104
106
  }
105
107
  }
106
108
  }
@@ -5,7 +5,7 @@
5
5
  import type { DeliveryState } from "./contract.js";
6
6
  import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
7
  /** Fields allowed on a durable outbox operation (privacy allowlist). */
8
- export declare const OUTBOX_ALLOWLIST: readonly ["contractVersion", "operationId", "clientOperationId", "sessionId", "harness", "companyUid", "companySlug", "projectId", "taskId", "companyConfidence", "kind", "digest", "delivery", "createdAt", "updatedAt", "attemptCount", "lastErrorCode", "receiptId", "destinationCompanyUid", "destinationCompanySlug", "nextAttemptAt"];
8
+ export declare const OUTBOX_ALLOWLIST: readonly ["contractVersion", "operationId", "clientOperationId", "sessionId", "harness", "companyUid", "companySlug", "projectId", "taskId", "companyConfidence", "kind", "digest", "delivery", "createdAt", "updatedAt", "attemptCount", "lastErrorCode", "lastErrorHttpStatus", "lastServerErrorCode", "receiptId", "destinationCompanyUid", "destinationCompanySlug", "nextAttemptAt"];
9
9
  /** Base delay for outbox retry backoff (attempt 1 → 30s). */
10
10
  export declare const OUTBOX_RETRY_BASE_MS = 30000;
11
11
  /** Cap for outbox retry backoff (6 hours). */
@@ -34,6 +34,8 @@ export interface OutboxOperation {
34
34
  updatedAt: string;
35
35
  attemptCount: number;
36
36
  lastErrorCode?: string;
37
+ lastErrorHttpStatus?: number;
38
+ lastServerErrorCode?: string;
37
39
  receiptId?: string;
38
40
  /** Destination company for kind=migrate (source is companyUid). */
39
41
  destinationCompanyUid?: string;
@@ -88,8 +90,13 @@ export declare function markOutboxAcked(operationId: string, root: string, recei
88
90
  export declare function writeAckedRegisterState(op: OutboxOperation, root: string, receiptId: string, now?: () => Date): void;
89
91
  /** Repair older unresolved session projections from acknowledged registrations. */
90
92
  export declare function repairAckedRegisterStates(root: string, sessionIds?: ReadonlySet<string>, now?: () => Date): number;
91
- export declare function markOutboxQueued(operationId: string, root: string, errorCode: string, now?: () => Date, random?: () => number): OutboxOperation | null;
92
- export declare function markOutboxQuarantined(operationId: string, root: string, errorCode: string, now?: () => Date): OutboxOperation | null;
93
+ export declare function markOutboxQueued(operationId: string, root: string, errorCode: string, now?: () => Date, random?: () => number, details?: OutboxErrorDetails): OutboxOperation | null;
94
+ export declare function markOutboxQuarantined(operationId: string, root: string, errorCode: string, now?: () => Date, details?: OutboxErrorDetails): OutboxOperation | null;
95
+ export interface OutboxErrorDetails {
96
+ httpStatus?: number;
97
+ serverCode?: string;
98
+ }
99
+ export declare function sanitizeOutboxErrorDetails(details?: OutboxErrorDetails): OutboxErrorDetails;
93
100
  /**
94
101
  * Requeue quarantined outbox operations so the daemon's replay picks them up
95
102
  * again. Quarantine is otherwise terminal: after the work-mesh-live gap 10 fix
@@ -119,16 +126,18 @@ export interface OutboxStats {
119
126
  queued: number;
120
127
  acked: number;
121
128
  lastQuarantineReason: string | null;
129
+ lastQuarantineHttpStatus: number | null;
130
+ lastQuarantineServerCode: string | null;
122
131
  }
123
132
  export declare function outboxStats(root: string): OutboxStats;
124
133
  export type DeliverFn = (op: OutboxOperation) => Promise<{
125
134
  ok: true;
126
135
  receiptId: string;
127
- } | {
136
+ } | ({
128
137
  ok: false;
129
138
  retryable: boolean;
130
139
  code: string;
131
- }>;
140
+ } & OutboxErrorDetails)>;
132
141
  /**
133
142
  * Replay queued (and recover lost-receipt) operations.
134
143
  * FIFO per sessionId; bounded parallelism across sessions.
@@ -8,6 +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 { isWorkSessionDenialCode } from "../mesh/denial-codes.js";
11
12
  import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
12
13
  import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
13
14
  /** Fields allowed on a durable outbox operation (privacy allowlist). */
@@ -29,6 +30,8 @@ export const OUTBOX_ALLOWLIST = [
29
30
  "updatedAt",
30
31
  "attemptCount",
31
32
  "lastErrorCode",
33
+ "lastErrorHttpStatus",
34
+ "lastServerErrorCode",
32
35
  "receiptId",
33
36
  "destinationCompanyUid",
34
37
  "destinationCompanySlug",
@@ -101,6 +104,11 @@ function projectOutbox(op) {
101
104
  out.companyConfidence = op.companyConfidence;
102
105
  if (op.lastErrorCode)
103
106
  out.lastErrorCode = op.lastErrorCode;
107
+ if (op.lastErrorHttpStatus !== undefined) {
108
+ out.lastErrorHttpStatus = op.lastErrorHttpStatus;
109
+ }
110
+ if (op.lastServerErrorCode)
111
+ out.lastServerErrorCode = op.lastServerErrorCode;
104
112
  if (op.receiptId)
105
113
  out.receiptId = op.receiptId;
106
114
  if (op.destinationCompanyUid) {
@@ -337,12 +345,13 @@ export function repairAckedRegisterStates(root, sessionIds, now = () => new Date
337
345
  }
338
346
  return repaired;
339
347
  }
340
- export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random) {
348
+ export function markOutboxQueued(operationId, root, errorCode, now = () => new Date(), random = Math.random, details) {
341
349
  const op = readOutboxOperation(operationId, root);
342
350
  if (!op)
343
351
  return null;
344
352
  op.delivery = "queued";
345
353
  op.lastErrorCode = errorCode;
354
+ applyOutboxErrorDetails(op, details);
346
355
  op.attemptCount += 1;
347
356
  const at = now();
348
357
  op.updatedAt = at.toISOString();
@@ -351,17 +360,40 @@ export function markOutboxQueued(operationId, root, errorCode, now = () => new D
351
360
  updateOutboxOperation(op, root);
352
361
  return op;
353
362
  }
354
- export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date()) {
363
+ export function markOutboxQuarantined(operationId, root, errorCode, now = () => new Date(), details) {
355
364
  const op = readOutboxOperation(operationId, root);
356
365
  if (!op)
357
366
  return null;
358
367
  op.delivery = "quarantined";
359
368
  op.lastErrorCode = errorCode;
369
+ applyOutboxErrorDetails(op, details);
360
370
  op.attemptCount += 1;
361
371
  op.updatedAt = now().toISOString();
362
372
  updateOutboxOperation(op, root);
363
373
  return op;
364
374
  }
375
+ function applyOutboxErrorDetails(op, details) {
376
+ delete op.lastErrorHttpStatus;
377
+ delete op.lastServerErrorCode;
378
+ const safe = sanitizeOutboxErrorDetails(details);
379
+ if (safe.httpStatus !== undefined)
380
+ op.lastErrorHttpStatus = safe.httpStatus;
381
+ if (safe.serverCode !== undefined)
382
+ op.lastServerErrorCode = safe.serverCode;
383
+ }
384
+ export function sanitizeOutboxErrorDetails(details) {
385
+ const safe = {};
386
+ if (details?.httpStatus !== undefined &&
387
+ Number.isInteger(details.httpStatus) &&
388
+ details.httpStatus >= 100 &&
389
+ details.httpStatus <= 599) {
390
+ safe.httpStatus = details.httpStatus;
391
+ }
392
+ if (isWorkSessionDenialCode(details?.serverCode)) {
393
+ safe.serverCode = details.serverCode;
394
+ }
395
+ return safe;
396
+ }
365
397
  /**
366
398
  * Requeue quarantined outbox operations so the daemon's replay picks them up
367
399
  * again. Quarantine is otherwise terminal: after the work-mesh-live gap 10 fix
@@ -389,6 +421,8 @@ export function requeueQuarantinedOutbox(root, opts = {}) {
389
421
  op.delivery = "queued";
390
422
  op.attemptCount = 0;
391
423
  op.lastErrorCode = undefined;
424
+ op.lastErrorHttpStatus = undefined;
425
+ op.lastServerErrorCode = undefined;
392
426
  op.nextAttemptAt = undefined;
393
427
  op.updatedAt = now().toISOString();
394
428
  updateOutboxOperation(op, root);
@@ -469,6 +503,15 @@ export function outboxStats(root) {
469
503
  queued: ops.filter((o) => o.delivery === "queued").length,
470
504
  acked: ops.filter((o) => o.delivery === "acked").length,
471
505
  lastQuarantineReason: latestQuarantine?.lastErrorCode ?? null,
506
+ lastQuarantineHttpStatus: latestQuarantine &&
507
+ Number.isInteger(latestQuarantine.lastErrorHttpStatus) &&
508
+ latestQuarantine.lastErrorHttpStatus >= 100 &&
509
+ latestQuarantine.lastErrorHttpStatus <= 599
510
+ ? latestQuarantine.lastErrorHttpStatus
511
+ : null,
512
+ lastQuarantineServerCode: isWorkSessionDenialCode(latestQuarantine?.lastServerErrorCode)
513
+ ? latestQuarantine.lastServerErrorCode
514
+ : null,
472
515
  };
473
516
  }
474
517
  function isOutboxDue(op, nowIso) {
@@ -551,18 +594,18 @@ export async function replayOutbox(root, deliver, opts = {}) {
551
594
  }
552
595
  else if (result.retryable) {
553
596
  if (op.attemptCount + 1 >= OUTBOX_MAX_ATTEMPTS) {
554
- markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now);
597
+ markOutboxQuarantined(op.operationId, root, `${result.code}_MAX_ATTEMPTS`, now, result);
555
598
  quarantined += 1;
556
599
  }
557
600
  else {
558
- markOutboxQueued(op.operationId, root, result.code, now, random);
601
+ markOutboxQueued(op.operationId, root, result.code, now, random, result);
559
602
  queued += 1;
560
603
  }
561
604
  // Stop this session's FIFO on transient failure.
562
605
  break;
563
606
  }
564
607
  else {
565
- markOutboxQuarantined(op.operationId, root, result.code, now);
608
+ markOutboxQuarantined(op.operationId, root, result.code, now, result);
566
609
  quarantined += 1;
567
610
  break;
568
611
  }
@@ -14,7 +14,7 @@ import { refineProjectBinding } from "./bind-best.js";
14
14
  import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
15
15
  import { resolveProjectTask, shouldAskAfter } from "./project.js";
16
16
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
17
- import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
17
+ import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, sanitizeOutboxErrorDetails, writeAckedRegisterState, } from "./outbox.js";
18
18
  import { mergeLocalSessionFields, isRegisterReceiptEpisodeId, readSessionState, stripRegisterReceiptEpisodes, writeSessionState, } from "./state.js";
19
19
  function resultOf(parts) {
20
20
  const out = {
@@ -38,6 +38,10 @@ function resultOf(parts) {
38
38
  out.bindingEpisodeId = parts.bindingEpisodeId;
39
39
  if (parts.errorCode)
40
40
  out.errorCode = parts.errorCode;
41
+ if (parts.httpStatus !== undefined)
42
+ out.httpStatus = parts.httpStatus;
43
+ if (parts.serverCode)
44
+ out.serverCode = parts.serverCode;
41
45
  if (parts.projectBindReason)
42
46
  out.projectBindReason = parts.projectBindReason;
43
47
  if (parts.projectCandidates)
@@ -974,7 +978,8 @@ export async function reconcileObservation(obs, deps) {
974
978
  };
975
979
  }
976
980
  if (delivered.retryable) {
977
- markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn);
981
+ const details = sanitizeOutboxErrorDetails(delivered);
982
+ markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn, undefined, details);
978
983
  return {
979
984
  result: resultOf({
980
985
  kind: "queued",
@@ -986,15 +991,20 @@ export async function reconcileObservation(obs, deps) {
986
991
  projectId,
987
992
  taskId,
988
993
  errorCode: delivered.code,
994
+ ...details,
989
995
  ...bindResultFields,
990
996
  }),
991
997
  exitCode: EXIT_OK,
992
- diagnostics: [...diagnostics, `transient ${delivered.code}; remains queued`],
998
+ diagnostics: [
999
+ ...diagnostics,
1000
+ `transient ${delivered.code}${formatDeliveryDetails(details)}; remains queued`,
1001
+ ],
993
1002
  statePath,
994
1003
  outboxOperationId: outboxOp.operationId,
995
1004
  };
996
1005
  }
997
- markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn);
1006
+ const details = sanitizeOutboxErrorDetails(delivered);
1007
+ markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn, details);
998
1008
  return {
999
1009
  result: resultOf({
1000
1010
  kind: "error",
@@ -1007,13 +1017,24 @@ export async function reconcileObservation(obs, deps) {
1007
1017
  projectId,
1008
1018
  taskId,
1009
1019
  errorCode: delivered.code,
1020
+ ...details,
1010
1021
  ...bindResultFields,
1011
1022
  }),
1012
1023
  exitCode: EXIT_OK,
1013
- diagnostics: [...diagnostics, `quarantined: ${delivered.code}`],
1024
+ diagnostics: [
1025
+ ...diagnostics,
1026
+ `quarantined: ${delivered.code}${formatDeliveryDetails(details)}`,
1027
+ ],
1014
1028
  statePath,
1015
1029
  outboxOperationId: outboxOp.operationId,
1016
1030
  };
1017
1031
  }
1032
+ function formatDeliveryDetails(details) {
1033
+ const parts = [
1034
+ details.httpStatus !== undefined ? `HTTP ${details.httpStatus}` : "",
1035
+ details.serverCode ?? "",
1036
+ ].filter(Boolean);
1037
+ return parts.length ? ` (${parts.join(" ")})` : "";
1038
+ }
1018
1039
  export { replayOutbox };
1019
1040
  //# sourceMappingURL=reconcile.js.map
package/dist/main.d.ts CHANGED
@@ -12,6 +12,25 @@ import { upgradeToTeam } from "./utils/team-upgrade.js";
12
12
  * finalization; command execution itself does not consume this allowance.
13
13
  */
14
14
  export declare const FOREGROUND_NETWORK_BUDGET_MS = 1500;
15
+ /**
16
+ * Shared accounting for awaited foreground network work. Each lifecycle phase
17
+ * debits only the bounded await it performs; ordinary command runtime leaves
18
+ * the allowance untouched. Earlier phases reserve the finalization floor, so
19
+ * error flushing cannot disappear after a long command.
20
+ */
21
+ declare class ForegroundNetworkBudget {
22
+ private readonly budgetMs;
23
+ private readonly now;
24
+ private consumedMs;
25
+ constructor(budgetMs?: number, now?: () => number);
26
+ timeoutFor(phaseMaximumMs: number, reservedMs?: number): number;
27
+ waitFor<T>(phaseMaximumMs: number, reservedMs: number, work: (timeoutMs: number) => Promise<T>): Promise<T | undefined>;
28
+ }
29
+ /** Create a shared invocation budget; exported so the time accounting is directly testable. */
30
+ export declare function createForegroundNetworkBudget(options?: {
31
+ budgetMs?: number;
32
+ now?: () => number;
33
+ }): ForegroundNetworkBudget;
15
34
  export type StreamErrorDependencies = {
16
35
  stderr: Pick<typeof process.stderr, "write">;
17
36
  exit: (code: number) => void;
@@ -30,4 +49,5 @@ export type TopLevelErrorDependencies = {
30
49
  };
31
50
  /** Classify a top-level failure without making the CLI process boundary opaque to tests. */
32
51
  export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): Promise<void>;
52
+ export {};
33
53
  //# sourceMappingURL=main.d.ts.map
package/dist/main.js CHANGED
@@ -108,6 +108,13 @@ class ForegroundNetworkBudget {
108
108
  }
109
109
  }
110
110
  }
111
+ /** Create a shared invocation budget; exported so the time accounting is directly testable. */
112
+ export function createForegroundNetworkBudget(options = {}) {
113
+ if (options.budgetMs === undefined && options.now === undefined) {
114
+ return new ForegroundNetworkBudget();
115
+ }
116
+ return new ForegroundNetworkBudget(options.budgetMs ?? FOREGROUND_NETWORK_BUDGET_MS, options.now ?? (() => performance.now()));
117
+ }
111
118
  let activeForegroundNetworkBudget;
112
119
  /**
113
120
  * The RUNNING install's own entrypoint (`<pkg>/dist/index.js`), used as the
@@ -193,7 +200,7 @@ program.hook("preAction", async () => {
193
200
  });
194
201
  });
195
202
  export async function runCli() {
196
- const foregroundNetworkBudget = new ForegroundNetworkBudget();
203
+ const foregroundNetworkBudget = createForegroundNetworkBudget();
197
204
  activeForegroundNetworkBudget = foregroundNetworkBudget;
198
205
  // Login must be usable before any identity or company is available, so do
199
206
  // not make a pre-auth feature-flag request on this path. Other commands
@@ -0,0 +1,4 @@
1
+ /** Return shell-compatible 0/1/2: true, false, or malformed. */
2
+ export declare function evaluateTrigger(expr: string, facts?: string): number;
3
+ export declare function checkTriggerExpressions(stdin: string): string;
4
+ //# sourceMappingURL=core-eval-trigger.d.ts.map