@indigoai-us/hq-cli 5.149.1 → 5.151.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.
@@ -844,6 +844,8 @@ export function markSpawnFailure(hqRoot, laneId, reasonRaw, details) {
844
844
  ...current,
845
845
  state: "failed",
846
846
  queued_reason: undefined,
847
+ queued_at: undefined,
848
+ admission_reserved_at: undefined,
847
849
  terminal_reason: reason,
848
850
  terminal_details: details,
849
851
  };
@@ -1360,6 +1362,8 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1360
1362
  ...current,
1361
1363
  state: "running",
1362
1364
  queued_reason: undefined,
1365
+ queued_at: undefined,
1366
+ admission_reserved_at: undefined,
1363
1367
  // An account move's reservation ends with the claim.
1364
1368
  pending_resume: undefined,
1365
1369
  account: resolved.id,
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { generateUlid } from "../mesh/live/ulid.js";
14
14
  import { resolveLanesHqRoot } from "./hq-root.js";
15
15
  import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
16
- import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
16
+ import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
17
17
  import { laneStories } from "./stories.js";
18
18
  export const LANES_STORE_REL = path.join("workspace", "lanes");
19
19
  export const LANE_RECORDS_DIR = "lanes";
@@ -245,6 +245,25 @@ function requireString(value, field) {
245
245
  }
246
246
  return value;
247
247
  }
248
+ function requireTimestamp(value, field, filePath) {
249
+ const parsedAt = typeof value === "string" ? Date.parse(value) : NaN;
250
+ const datePart = typeof value === "string" ? value.slice(0, 10) : "";
251
+ const datePartAt = /^\d{4}-\d{2}-\d{2}$/.test(datePart)
252
+ ? Date.parse(`${datePart}T00:00:00.000Z`)
253
+ : NaN;
254
+ if (typeof value !== "string" ||
255
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) ||
256
+ !Number.isFinite(parsedAt) ||
257
+ !Number.isFinite(datePartAt) ||
258
+ new Date(datePartAt).toISOString().slice(0, 10) !== datePart) {
259
+ throw new LaneSchemaError(`Expected ISO timestamp for ${field}`, {
260
+ path: filePath,
261
+ field,
262
+ value,
263
+ });
264
+ }
265
+ return value;
266
+ }
248
267
  function requireInt(value, field) {
249
268
  if (typeof value !== "number" || !Number.isInteger(value)) {
250
269
  throw new LaneSchemaError(`Expected integer for ${field}`, { field });
@@ -371,6 +390,31 @@ export function parseLaneRecord(raw, filePath) {
371
390
  requireInt(raw.priority, "priority");
372
391
  requireString(raw.project_id, "project_id");
373
392
  requireString(raw.story_id, "story_id");
393
+ requireTimestamp(raw.created_at, "created_at", filePath);
394
+ requireTimestamp(raw.updated_at, "updated_at", filePath);
395
+ if (raw.queued_reason !== undefined && !isQueuedReason(raw.queued_reason)) {
396
+ throw new LaneSchemaError(`Unknown queued reason ${JSON.stringify(raw.queued_reason)}`, {
397
+ path: filePath,
398
+ field: "queued_reason",
399
+ value: raw.queued_reason,
400
+ });
401
+ }
402
+ if (raw.queued_at !== undefined)
403
+ requireTimestamp(raw.queued_at, "queued_at", filePath);
404
+ if (raw.admission_reserved_at !== undefined) {
405
+ requireTimestamp(raw.admission_reserved_at, "admission_reserved_at", filePath);
406
+ }
407
+ if (raw.admission_accounts !== undefined) {
408
+ if (!Array.isArray(raw.admission_accounts) ||
409
+ raw.admission_accounts.length === 0 ||
410
+ raw.admission_accounts.some((account) => typeof account !== "string" || account.trim() === "") ||
411
+ new Set(raw.admission_accounts).size !== raw.admission_accounts.length) {
412
+ throw new LaneSchemaError("admission_accounts must be a non-empty array of unique account ids", { path: filePath, field: "admission_accounts" });
413
+ }
414
+ if (typeof raw.account !== "string" || !raw.admission_accounts.includes(raw.account)) {
415
+ throw new LaneSchemaError("account must be present in admission_accounts", { path: filePath, field: "admission_accounts", account: raw.account });
416
+ }
417
+ }
374
418
  if (!isLaneState(raw.state)) {
375
419
  throw new LaneSchemaError(`Unknown lane state ${JSON.stringify(raw.state)}`, { path: filePath, value: raw.state });
376
420
  }
@@ -704,6 +748,9 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
704
748
  version: 1,
705
749
  created_at: init.created_at ?? now,
706
750
  updated_at: init.updated_at ?? now,
751
+ ...(init.state === "queued"
752
+ ? { queued_at: init.queued_at ?? init.created_at ?? now }
753
+ : {}),
707
754
  senior_history: init.senior_history ?? [],
708
755
  attribution_history: init.attribution_history ?? [],
709
756
  };
@@ -166,8 +166,9 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
166
166
  export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
167
167
  export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
168
168
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
169
- export declare const QUEUED_REASONS: readonly ["cap", "queue_position", "throttle", "provider_binary_missing"];
169
+ export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing"];
170
170
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
171
+ export declare function isQueuedReason(value: unknown): value is QueuedReason;
171
172
  export declare const HOST_KINDS: readonly ["local", "outpost"];
172
173
  export type HostKind = (typeof HOST_KINDS)[number];
173
174
  export declare const SENIOR_KINDS: readonly ["session", "lane", "user"];
@@ -270,6 +271,10 @@ export interface LaneRecord {
270
271
  senior_history: SeniorHistoryEntry[];
271
272
  state: LaneState;
272
273
  queued_reason?: QueuedReason;
274
+ /** Time the current queue turn began; old records fall back to created_at. */
275
+ queued_at?: string;
276
+ /** Short-lived slot reservation between create persistence and worker spawn. */
277
+ admission_reserved_at?: string;
273
278
  /**
274
279
  * Set by `hq lanes pickup` when it stores an outpost lane and cleared by
275
280
  * the account bind once the model probe has finished. While set, the
@@ -293,6 +298,8 @@ export interface LaneRecord {
293
298
  worker_profile_ignored?: string[];
294
299
  provider: LaneProvider;
295
300
  account?: string;
301
+ /** Eligible accounts for a queue entry created without an explicit account. */
302
+ admission_accounts?: string[];
296
303
  /**
297
304
  * Every operator move of the lane's session to another account of the
298
305
  * same provider (`hq lanes resume|message --account`), newest last, capped
@@ -95,7 +95,18 @@ export const TERMINAL_LANE_STATES = [
95
95
  "killed",
96
96
  ];
97
97
  export const LANE_PROVIDERS = ["claude", "codex", "grok"];
98
- export const QUEUED_REASONS = ["cap", "queue_position", "throttle", "provider_binary_missing"];
98
+ export const QUEUED_REASONS = [
99
+ "cap",
100
+ "provider_cap",
101
+ "account_cap",
102
+ "profile_cap",
103
+ "queue_position",
104
+ "throttle",
105
+ "provider_binary_missing",
106
+ ];
107
+ export function isQueuedReason(value) {
108
+ return typeof value === "string" && QUEUED_REASONS.includes(value);
109
+ }
99
110
  export const HOST_KINDS = ["local", "outpost"];
100
111
  export const SENIOR_KINDS = ["session", "lane", "user"];
101
112
  export const ENVELOPE_DECISIONS = ["done", "ask", "blocked"];
@@ -204,16 +215,16 @@ export const LANE_FIELD_WRITERS = {
204
215
  schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
205
216
  project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator"],
206
217
  depth: ["command"], senior: ["command"], senior_history: ["command"],
207
- state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "reconcile"],
218
+ state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "reconcile"], queued_at: ["creator", "command", "spawn"], admission_reserved_at: ["creator", "command", "reconcile", "spawn"],
208
219
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
209
220
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
210
221
  priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"], worker_profile: ["creator"], worker_profile_hash: ["creator", "spawn"], worker_profile_ignored: ["creator", "spawn"],
211
- provider: ["creator"], account: ["creator", "spawn", "command"], account_history: ["command"], pending_resume: ["command", "spawn"], host: ["creator"], origin_host: ["creator"], origin_host_source: ["creator"],
222
+ provider: ["creator"], account: ["creator", "spawn", "command", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command"], pending_resume: ["command", "spawn"], host: ["creator"], origin_host: ["creator"], origin_host_source: ["creator"],
212
223
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
213
224
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
214
225
  last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
215
226
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
216
- admitted_reason: ["reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
227
+ admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
217
228
  quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
218
229
  provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], last_progress_event_at: ["watcher"],
219
230
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.149.1",
3
+ "version": "5.151.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {