@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.
- package/CHANGELOG.md +9 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
- package/assets/scaffold/core/scripts/register-project.sh +262 -0
- package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
- package/dist/command-catalog.generated.d.ts +80 -0
- package/dist/command-catalog.generated.js +99 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/company-rename-flag.d.ts +4 -0
- package/dist/commands/company-rename-flag.js +7 -0
- package/dist/commands/company-rename.d.ts +4 -0
- package/dist/commands/company-rename.js +90 -0
- package/dist/commands/company.js +2 -0
- package/dist/commands/core-eval-trigger-native.d.ts +4 -0
- package/dist/commands/core-eval-trigger-native.js +12 -0
- package/dist/commands/core-knowledge-prefs.d.ts +0 -1
- package/dist/commands/core-knowledge-prefs.js +1 -1
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
- package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
- package/dist/commands/core-native-hook-helper-commands.js +56 -0
- package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
- package/dist/commands/core-native-hook-helpers-flag.js +7 -0
- package/dist/commands/core-register-project-native.d.ts +30 -0
- package/dist/commands/core-register-project-native.js +347 -0
- package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
- package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
- package/dist/commands/core-worker-share-native.d.ts +0 -1
- package/dist/commands/core-worker-share-native.js +1 -1
- package/dist/commands/core.js +7 -0
- package/dist/commands/lanes-message.js +3 -0
- package/dist/commands/lanes-questions.js +2 -1
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +151 -21
- package/dist/fast-core.js +10 -3
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +11 -0
- package/dist/lib/doctor/checks/work-context.js +9 -1
- package/dist/lib/lanes/config.d.ts +3 -0
- package/dist/lib/lanes/config.js +14 -0
- package/dist/lib/lanes/disk-guard.d.ts +66 -0
- package/dist/lib/lanes/disk-guard.js +172 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/grants.d.ts +4 -3
- package/dist/lib/lanes/grants.js +12 -7
- package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +142 -11
- package/dist/lib/lanes/registry-flag.d.ts +6 -0
- package/dist/lib/lanes/registry-flag.js +25 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +26 -1
- package/dist/lib/lanes/store.js +11 -0
- package/dist/lib/lanes/types.d.ts +11 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/client.js +23 -5
- package/dist/lib/mesh/denial-codes.d.ts +5 -0
- package/dist/lib/mesh/denial-codes.js +14 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +11 -1
- package/dist/lib/search-index/embed-lock.d.ts +2 -0
- package/dist/lib/search-index/embed-lock.js +10 -1
- package/dist/lib/work-context/contract.d.ts +3 -0
- package/dist/lib/work-context/contract.schema.json +3 -1
- package/dist/lib/work-context/outbox.d.ts +14 -5
- package/dist/lib/work-context/outbox.js +48 -5
- package/dist/lib/work-context/reconcile.js +26 -5
- package/dist/main.d.ts +20 -0
- package/dist/main.js +8 -1
- package/dist/utils/core-eval-trigger.d.ts +4 -0
- package/dist/utils/core-eval-trigger.js +88 -0
- package/dist/utils/pack-contributions.d.ts +0 -5
- package/dist/utils/pack-contributions.js +1 -1
- package/dist/utils/redact-error-text.d.ts +0 -2
- package/dist/utils/redact-error-text.js +1 -1
- package/dist/utils/sandbox-runner-client.d.ts +2 -1
- 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. */
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -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,
|
|
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
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -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. */
|
package/dist/lib/lanes/types.js
CHANGED
package/dist/lib/mesh/client.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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: [
|
|
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 =
|
|
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
|