@bli-cockpit/cli 0.2.54 → 0.2.56
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/dist/adapters/attribution-core-fallbacks.js +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- package/package.json +3 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { parseMetadataOnlySessionReportRows } from "./local-spool-parse.js";
|
|
3
|
+
import { readLocalUploadSpoolState, writeUploadSpoolState } from "./local-spool-io.js";
|
|
4
|
+
import { MAX_PENDING_UPLOADS, } from "./local-spool-types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Every state transition a sync tick can record: a blocked or failed upload
|
|
7
|
+
* attempt, a success that clears pending entries, a source that needs a
|
|
8
|
+
* wider re-scan next time, and a required session-attribution report that
|
|
9
|
+
* could not be delivered yet. Each function reads the current state, applies
|
|
10
|
+
* one transition, and writes the result back — the read-modify-write pair
|
|
11
|
+
* always happens together so no caller can race a stale write.
|
|
12
|
+
*/
|
|
13
|
+
export async function recordUploadBlocked(paths, options) {
|
|
14
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
15
|
+
const next = {
|
|
16
|
+
...state,
|
|
17
|
+
updated_at: options.attemptedAt,
|
|
18
|
+
last_upload_attempt_at: options.attemptedAt,
|
|
19
|
+
last_upload_failure_reason: options.reason,
|
|
20
|
+
};
|
|
21
|
+
await writeUploadSpoolState(paths, next);
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
export async function recordUploadSuccess(paths, options) {
|
|
25
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
26
|
+
const pending = options.clearPendingForContext === false
|
|
27
|
+
? state.pending_uploads
|
|
28
|
+
: state.pending_uploads.filter((entry) => entry.work_context_id !== options.workContextId);
|
|
29
|
+
const next = {
|
|
30
|
+
...state,
|
|
31
|
+
updated_at: options.attemptedAt,
|
|
32
|
+
last_upload_attempt_at: options.attemptedAt,
|
|
33
|
+
last_upload_success_at: options.attemptedAt,
|
|
34
|
+
last_upload_failure_reason: firstPendingFailureReason({
|
|
35
|
+
...state,
|
|
36
|
+
pending_uploads: pending,
|
|
37
|
+
}),
|
|
38
|
+
pending_uploads: pending,
|
|
39
|
+
};
|
|
40
|
+
await writeUploadSpoolState(paths, next);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
export async function recordUploadFailure(paths, entry) {
|
|
44
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
45
|
+
const createdAt = entry.created_at ?? entry.last_attempt_at;
|
|
46
|
+
const spoolEntry = {
|
|
47
|
+
spool_id: `upload-${crypto.randomUUID()}`,
|
|
48
|
+
created_at: createdAt,
|
|
49
|
+
...entry,
|
|
50
|
+
};
|
|
51
|
+
const pending = [
|
|
52
|
+
spoolEntry,
|
|
53
|
+
...state.pending_uploads.filter((candidate) => candidate.work_context_id !== spoolEntry.work_context_id ||
|
|
54
|
+
candidate.ticket_id !== spoolEntry.ticket_id),
|
|
55
|
+
].slice(0, MAX_PENDING_UPLOADS);
|
|
56
|
+
await writeUploadSpoolState(paths, {
|
|
57
|
+
...state,
|
|
58
|
+
updated_at: entry.last_attempt_at,
|
|
59
|
+
last_upload_attempt_at: entry.last_attempt_at,
|
|
60
|
+
last_upload_failure_reason: entry.failure_reason,
|
|
61
|
+
pending_uploads: pending,
|
|
62
|
+
});
|
|
63
|
+
return spoolEntry;
|
|
64
|
+
}
|
|
65
|
+
export async function recordSourceRetryFailure(paths, options) {
|
|
66
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
67
|
+
const retryEntry = {
|
|
68
|
+
source: options.source,
|
|
69
|
+
reason: options.reason,
|
|
70
|
+
last_attempt_at: options.attemptedAt,
|
|
71
|
+
};
|
|
72
|
+
const next = {
|
|
73
|
+
...state,
|
|
74
|
+
updated_at: options.attemptedAt,
|
|
75
|
+
last_upload_attempt_at: options.attemptedAt,
|
|
76
|
+
last_upload_failure_reason: options.reason,
|
|
77
|
+
pending_source_retries: [
|
|
78
|
+
retryEntry,
|
|
79
|
+
...state.pending_source_retries.filter((entry) => entry.source !== options.source),
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
await writeUploadSpoolState(paths, next);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
export async function clearSourceRetryFailure(paths, source, attemptedAt) {
|
|
86
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
87
|
+
const next = {
|
|
88
|
+
...state,
|
|
89
|
+
updated_at: attemptedAt,
|
|
90
|
+
pending_source_retries: state.pending_source_retries.filter((entry) => entry.source !== source),
|
|
91
|
+
};
|
|
92
|
+
next.last_upload_failure_reason = firstPendingFailureReason(next);
|
|
93
|
+
await writeUploadSpoolState(paths, next);
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
export async function recordPendingSessionReport(paths, entry) {
|
|
97
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
98
|
+
const existing = state.pending_session_reports.find((candidate) => candidate.work_context_id === entry.work_context_id);
|
|
99
|
+
const safeIncomingSessions = parseMetadataOnlySessionReportRows(entry.sessions, "pending_session_report.sessions");
|
|
100
|
+
const mergedSessions = mergeSessionReportRows(existing?.sessions ?? [], safeIncomingSessions);
|
|
101
|
+
const pending = {
|
|
102
|
+
report_id: existing?.report_id ?? `session-report-${crypto.randomUUID()}`,
|
|
103
|
+
created_at: existing?.created_at ?? entry.attempted_at,
|
|
104
|
+
last_attempt_at: entry.attempted_at,
|
|
105
|
+
dashboard_url: entry.dashboard_url,
|
|
106
|
+
generated_at: entry.generated_at,
|
|
107
|
+
work_context_id: entry.work_context_id,
|
|
108
|
+
repo_label: entry.repo_label,
|
|
109
|
+
branch: entry.branch,
|
|
110
|
+
repo_fingerprint: entry.repo_fingerprint,
|
|
111
|
+
repo_origin_url: entry.repo_origin_url,
|
|
112
|
+
worktree_label: entry.worktree_label,
|
|
113
|
+
worktree_fingerprint: entry.worktree_fingerprint,
|
|
114
|
+
worktree_is_primary: entry.worktree_is_primary,
|
|
115
|
+
sessions: mergedSessions,
|
|
116
|
+
failure_reason: entry.failure_reason ?? "session_report_pending",
|
|
117
|
+
};
|
|
118
|
+
const next = {
|
|
119
|
+
...state,
|
|
120
|
+
updated_at: entry.attempted_at,
|
|
121
|
+
last_upload_attempt_at: entry.attempted_at,
|
|
122
|
+
last_upload_failure_reason: pending.failure_reason,
|
|
123
|
+
pending_session_reports: [
|
|
124
|
+
pending,
|
|
125
|
+
...state.pending_session_reports.filter((candidate) => candidate.report_id !== pending.report_id),
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
await writeUploadSpoolState(paths, next);
|
|
129
|
+
return pending;
|
|
130
|
+
}
|
|
131
|
+
export async function recordSessionReportFailure(paths, options) {
|
|
132
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
133
|
+
const next = {
|
|
134
|
+
...state,
|
|
135
|
+
updated_at: options.attemptedAt,
|
|
136
|
+
last_upload_attempt_at: options.attemptedAt,
|
|
137
|
+
last_upload_failure_reason: options.reason,
|
|
138
|
+
pending_session_reports: state.pending_session_reports.map((entry) => entry.report_id === options.reportId
|
|
139
|
+
? {
|
|
140
|
+
...entry,
|
|
141
|
+
last_attempt_at: options.attemptedAt,
|
|
142
|
+
failure_reason: options.reason,
|
|
143
|
+
}
|
|
144
|
+
: entry),
|
|
145
|
+
};
|
|
146
|
+
await writeUploadSpoolState(paths, next);
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
export async function recordSessionReportSuccess(paths, options) {
|
|
150
|
+
const state = await readLocalUploadSpoolState(paths);
|
|
151
|
+
const next = {
|
|
152
|
+
...state,
|
|
153
|
+
updated_at: options.attemptedAt,
|
|
154
|
+
last_upload_attempt_at: options.attemptedAt,
|
|
155
|
+
last_upload_success_at: options.attemptedAt,
|
|
156
|
+
pending_session_reports: state.pending_session_reports.filter((entry) => entry.report_id !== options.reportId),
|
|
157
|
+
};
|
|
158
|
+
next.last_upload_failure_reason = firstPendingFailureReason(next);
|
|
159
|
+
await writeUploadSpoolState(paths, next);
|
|
160
|
+
return next;
|
|
161
|
+
}
|
|
162
|
+
function mergeSessionReportRows(existing, incoming) {
|
|
163
|
+
const bySession = new Map();
|
|
164
|
+
for (const session of [...existing, ...incoming]) {
|
|
165
|
+
bySession.set(`${session.source ?? "codex"}:${session.codex_session_id}`, session);
|
|
166
|
+
}
|
|
167
|
+
return [...bySession.values()];
|
|
168
|
+
}
|
|
169
|
+
function firstPendingFailureReason(state) {
|
|
170
|
+
return (state.pending_uploads[0]?.failure_reason ??
|
|
171
|
+
state.pending_source_retries[0]?.reason ??
|
|
172
|
+
state.pending_session_reports[0]?.failure_reason ??
|
|
173
|
+
null);
|
|
174
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CodexSessionAttributionSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
/**
|
|
3
|
+
* Validates a JSON-parsed spool file against the schema by hand (no zod
|
|
4
|
+
* dependency in this package): every field is required or explicitly
|
|
5
|
+
* nullable/optional, and a malformed field fails closed instead of being
|
|
6
|
+
* silently discarded — the spool holds delivery state a green sync must not
|
|
7
|
+
* pretend was empty.
|
|
8
|
+
*/
|
|
9
|
+
export function parseUploadSpoolState(value) {
|
|
10
|
+
const record = requireRecord(value, "root");
|
|
11
|
+
if (record["schema_version"] !== "cockpit-upload-spool.v1") {
|
|
12
|
+
invalidUploadSpoolState("schema_version", 'literal "cockpit-upload-spool.v1"');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
schema_version: "cockpit-upload-spool.v1",
|
|
16
|
+
updated_at: requireNullableString(record["updated_at"], "updated_at"),
|
|
17
|
+
last_upload_attempt_at: requireNullableString(record["last_upload_attempt_at"], "last_upload_attempt_at"),
|
|
18
|
+
last_upload_success_at: requireNullableString(record["last_upload_success_at"], "last_upload_success_at"),
|
|
19
|
+
last_upload_failure_reason: requireNullableString(record["last_upload_failure_reason"], "last_upload_failure_reason"),
|
|
20
|
+
pending_uploads: requireArray(record["pending_uploads"], "pending_uploads").map((entry, index) => parseUploadSpoolEntry(entry, `pending_uploads[${index}]`)),
|
|
21
|
+
// These fields were added without changing the v1 schema version. Missing
|
|
22
|
+
// fields are therefore a valid legacy state, while present malformed fields
|
|
23
|
+
// must fail closed instead of being silently discarded.
|
|
24
|
+
pending_source_retries: optionalArray(record["pending_source_retries"], "pending_source_retries").map((entry, index) => parseSourceRetryEntry(entry, `pending_source_retries[${index}]`)),
|
|
25
|
+
pending_session_reports: optionalArray(record["pending_session_reports"], "pending_session_reports").map((entry, index) => parsePendingSessionReport(entry, `pending_session_reports[${index}]`)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function parseUploadSpoolEntry(value, fieldPath) {
|
|
29
|
+
const record = requireRecord(value, fieldPath);
|
|
30
|
+
const retryCommand = record["retry_command"] === undefined
|
|
31
|
+
? "cockpit sync"
|
|
32
|
+
: requireString(record["retry_command"], `${fieldPath}.retry_command`);
|
|
33
|
+
return {
|
|
34
|
+
spool_id: requireString(record["spool_id"], `${fieldPath}.spool_id`),
|
|
35
|
+
created_at: requireString(record["created_at"], `${fieldPath}.created_at`),
|
|
36
|
+
last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
|
|
37
|
+
dashboard_url: requireString(record["dashboard_url"], `${fieldPath}.dashboard_url`),
|
|
38
|
+
work_context_id: requireNullableString(record["work_context_id"], `${fieldPath}.work_context_id`),
|
|
39
|
+
ticket_id: requireNullableString(record["ticket_id"], `${fieldPath}.ticket_id`),
|
|
40
|
+
repo_label: requireNullableString(record["repo_label"], `${fieldPath}.repo_label`),
|
|
41
|
+
branch: requireNullableString(record["branch"], `${fieldPath}.branch`),
|
|
42
|
+
event_count: requireNonNegativeInteger(record["event_count"], `${fieldPath}.event_count`),
|
|
43
|
+
source_scan_count: requireNonNegativeInteger(record["source_scan_count"], `${fieldPath}.source_scan_count`),
|
|
44
|
+
risk_flag_count: requireNonNegativeInteger(record["risk_flag_count"], `${fieldPath}.risk_flag_count`),
|
|
45
|
+
raw_evidence_file_count: requireNonNegativeInteger(record["raw_evidence_file_count"], `${fieldPath}.raw_evidence_file_count`),
|
|
46
|
+
retry_sources: parseRetrySources(record["retry_sources"], `${fieldPath}.retry_sources`),
|
|
47
|
+
failure_reason: requireString(record["failure_reason"], `${fieldPath}.failure_reason`),
|
|
48
|
+
retry_command: retryCommand,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function parseRetrySources(value, fieldPath) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
const values = requireArray(value, fieldPath);
|
|
55
|
+
return [...new Set(values.map((source) => parseRetrySource(source, fieldPath)))];
|
|
56
|
+
}
|
|
57
|
+
function parseSourceRetryEntry(value, fieldPath) {
|
|
58
|
+
const record = requireRecord(value, fieldPath);
|
|
59
|
+
return {
|
|
60
|
+
source: parseRetrySource(record["source"], `${fieldPath}.source`),
|
|
61
|
+
reason: requireString(record["reason"], `${fieldPath}.reason`),
|
|
62
|
+
last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parsePendingSessionReport(value, fieldPath) {
|
|
66
|
+
const record = requireRecord(value, fieldPath);
|
|
67
|
+
const sessions = parseMetadataOnlySessionReportRows(record["sessions"], `${fieldPath}.sessions`);
|
|
68
|
+
return {
|
|
69
|
+
report_id: requireString(record["report_id"], `${fieldPath}.report_id`),
|
|
70
|
+
created_at: requireString(record["created_at"], `${fieldPath}.created_at`),
|
|
71
|
+
last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
|
|
72
|
+
dashboard_url: requireString(record["dashboard_url"], `${fieldPath}.dashboard_url`),
|
|
73
|
+
generated_at: requireString(record["generated_at"], `${fieldPath}.generated_at`),
|
|
74
|
+
work_context_id: requireString(record["work_context_id"], `${fieldPath}.work_context_id`),
|
|
75
|
+
repo_label: requireString(record["repo_label"], `${fieldPath}.repo_label`),
|
|
76
|
+
branch: requireString(record["branch"], `${fieldPath}.branch`),
|
|
77
|
+
repo_fingerprint: requireString(record["repo_fingerprint"], `${fieldPath}.repo_fingerprint`),
|
|
78
|
+
repo_origin_url: record["repo_origin_url"] === undefined
|
|
79
|
+
? null
|
|
80
|
+
: requireNullableString(record["repo_origin_url"], `${fieldPath}.repo_origin_url`),
|
|
81
|
+
worktree_label: requireString(record["worktree_label"], `${fieldPath}.worktree_label`),
|
|
82
|
+
worktree_fingerprint: requireString(record["worktree_fingerprint"], `${fieldPath}.worktree_fingerprint`),
|
|
83
|
+
worktree_is_primary: requireBoolean(record["worktree_is_primary"], `${fieldPath}.worktree_is_primary`),
|
|
84
|
+
sessions,
|
|
85
|
+
failure_reason: requireString(record["failure_reason"], `${fieldPath}.failure_reason`),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function parseMetadataOnlySessionReportRows(value, fieldPath) {
|
|
89
|
+
const sessions = requireArray(value, fieldPath).map((session, index) => {
|
|
90
|
+
const parsed = CodexSessionAttributionSchema.safeParse(session);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
invalidUploadSpoolState(`${fieldPath}[${index}]`, "metadata-only Codex session attribution");
|
|
93
|
+
}
|
|
94
|
+
return parsed.data;
|
|
95
|
+
});
|
|
96
|
+
if (sessions.length === 0) {
|
|
97
|
+
invalidUploadSpoolState(fieldPath, "non-empty array");
|
|
98
|
+
}
|
|
99
|
+
return sessions;
|
|
100
|
+
}
|
|
101
|
+
function parseRetrySource(value, fieldPath) {
|
|
102
|
+
if (value === "codex" || value === "claude_code")
|
|
103
|
+
return value;
|
|
104
|
+
return invalidUploadSpoolState(fieldPath, '"codex" or "claude_code"');
|
|
105
|
+
}
|
|
106
|
+
function requireRecord(value, fieldPath) {
|
|
107
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
return invalidUploadSpoolState(fieldPath, "object");
|
|
111
|
+
}
|
|
112
|
+
function requireArray(value, fieldPath) {
|
|
113
|
+
if (Array.isArray(value))
|
|
114
|
+
return value;
|
|
115
|
+
return invalidUploadSpoolState(fieldPath, "array");
|
|
116
|
+
}
|
|
117
|
+
function optionalArray(value, fieldPath) {
|
|
118
|
+
return value === undefined ? [] : requireArray(value, fieldPath);
|
|
119
|
+
}
|
|
120
|
+
function requireString(value, fieldPath) {
|
|
121
|
+
if (typeof value === "string" && value.trim())
|
|
122
|
+
return value;
|
|
123
|
+
return invalidUploadSpoolState(fieldPath, "non-empty string");
|
|
124
|
+
}
|
|
125
|
+
function requireNullableString(value, fieldPath) {
|
|
126
|
+
if (value === null)
|
|
127
|
+
return null;
|
|
128
|
+
return requireString(value, fieldPath);
|
|
129
|
+
}
|
|
130
|
+
function requireNonNegativeInteger(value, fieldPath) {
|
|
131
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
return invalidUploadSpoolState(fieldPath, "non-negative integer");
|
|
135
|
+
}
|
|
136
|
+
function requireBoolean(value, fieldPath) {
|
|
137
|
+
if (typeof value === "boolean")
|
|
138
|
+
return value;
|
|
139
|
+
return invalidUploadSpoolState(fieldPath, "boolean");
|
|
140
|
+
}
|
|
141
|
+
function invalidUploadSpoolState(fieldPath, expected) {
|
|
142
|
+
throw new Error(`Upload spool state schema mismatch at ${fieldPath}; expected ${expected}.`);
|
|
143
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes and tuning constants `local-spool.ts` and its siblings (io,
|
|
3
|
+
* parse, mutations) all agree on. The spool is deliberately metadata-only —
|
|
4
|
+
* hashes, ids, reason labels, and durable pointer ids, never transcript
|
|
5
|
+
* bodies or local file paths — so a later sync can reconstruct provenance
|
|
6
|
+
* and post the exact rows even after the source transcript has aged out of
|
|
7
|
+
* the normal scan window.
|
|
8
|
+
*/
|
|
9
|
+
export const SPOOL_STATE_FILENAME = "upload-state.json";
|
|
10
|
+
export const MAX_PENDING_UPLOADS = 20;
|
|
11
|
+
export function emptyUploadSpoolState() {
|
|
12
|
+
return {
|
|
13
|
+
schema_version: "cockpit-upload-spool.v1",
|
|
14
|
+
updated_at: null,
|
|
15
|
+
last_upload_attempt_at: null,
|
|
16
|
+
last_upload_success_at: null,
|
|
17
|
+
last_upload_failure_reason: null,
|
|
18
|
+
pending_uploads: [],
|
|
19
|
+
pending_source_retries: [],
|
|
20
|
+
pending_session_reports: [],
|
|
21
|
+
};
|
|
22
|
+
}
|