@nurix/nustack 0.24.0 → 0.25.2

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 (48) hide show
  1. package/dist/commands/desktopAgent.js +1 -1
  2. package/dist/commands/doctor.js +8 -11
  3. package/dist/commands/obligationList.js +23 -49
  4. package/dist/commands/records.js +6 -88
  5. package/dist/commands/store.js +199 -0
  6. package/dist/commands/sync.js +2 -2
  7. package/dist/commands/toolkit.js +2 -2
  8. package/dist/lib/context/register.js +1 -1
  9. package/dist/lib/desktop-agents/checkout.js +1 -1
  10. package/dist/lib/desktop-agents/claudeRun.js +4 -4
  11. package/dist/lib/etna/reconcile.js +4 -4
  12. package/dist/lib/graph/library.js +1 -1
  13. package/dist/lib/harness/hookCheck.js +2 -8
  14. package/dist/lib/harness/storeWaivers.js +1 -4
  15. package/dist/lib/local_store/database.js +12 -92
  16. package/dist/lib/local_store/device_state.js +2 -2
  17. package/dist/lib/local_store/index.js +2 -2
  18. package/dist/lib/local_store/paths.js +0 -6
  19. package/dist/lib/local_store/routing.js +8 -44
  20. package/dist/lib/onboarding/folder_state.js +2 -2
  21. package/dist/lib/onboarding/init_fresh_folder.js +1 -1
  22. package/dist/lib/onboarding/manifest_writer.js +6 -6
  23. package/dist/lib/origin_guard.js +1 -1
  24. package/dist/lib/project_context.js +5 -5
  25. package/dist/lib/project_contract.js +2 -2
  26. package/dist/lib/records/drain.js +3 -153
  27. package/dist/lib/records/kinds.js +16 -0
  28. package/dist/lib/records/recordInput.js +1 -1
  29. package/dist/lib/records/recordsTable.js +119 -161
  30. package/dist/lib/records/refusalLog.js +7 -7
  31. package/dist/lib/sessions/brief.js +1 -1
  32. package/dist/lib/sessions/sessionEvents.js +5 -49
  33. package/dist/lib/store/open.js +136 -0
  34. package/dist/lib/store/wipe.js +34 -0
  35. package/dist/lib/sync/apply.js +113 -0
  36. package/dist/lib/sync/client.js +327 -0
  37. package/dist/lib/sync/endpoint.js +26 -0
  38. package/dist/lib/sync/manifest.js +80 -0
  39. package/dist/lib/sync/outbox.js +104 -0
  40. package/dist/machine.sql +934 -0
  41. package/dist/program.js +8 -6
  42. package/dist/records.schema.json +4 -0
  43. package/dist/spec_schema.json +5 -5
  44. package/dist/vendor/harness/rules/docsRoot.js +2 -2
  45. package/dist/vendor/harness/rules/index.js +1 -1
  46. package/package.json +2 -2
  47. package/dist/lib/local_store/migrations.js +0 -399
  48. package/dist/lib/records/door.js +0 -79
@@ -0,0 +1,34 @@
1
+ import { rmSync } from "node:fs";
2
+ export const UPGRADE_COMMAND = "npm i -g @nurix/nustack@latest";
3
+ export const WIPE_PUSH_TIMEOUT_MS = 3000;
4
+ export function versionVerdict(found, own) {
5
+ if (found === own)
6
+ return "same";
7
+ return found > own ? "newer" : "older";
8
+ }
9
+ const releaseOf = (stamp) => `${Math.floor(stamp / 1000)}.${stamp % 1000}`;
10
+ export class StoreTooNewError extends Error {
11
+ code = "STORE_TOO_NEW";
12
+ foundStamp;
13
+ ownStamp;
14
+ constructor(foundStamp, ownStamp) {
15
+ super(`This machine's NuStack store was created by NuStack ${releaseOf(foundStamp)}, newer than this CLI (${releaseOf(ownStamp)}). Update NuStack: ${UPGRADE_COMMAND}`);
16
+ this.name = "StoreTooNewError";
17
+ this.foundStamp = foundStamp;
18
+ this.ownStamp = ownStamp;
19
+ }
20
+ }
21
+ export async function wipeStaleStore(db, dbFile) {
22
+ try {
23
+ const pending = db.prepare("SELECT count(*) AS n FROM sync_outbox").get();
24
+ if (Number(pending?.n ?? 0) > 0) {
25
+ const { pushOutbox } = await import("../sync/client.js");
26
+ await pushOutbox({ db, close: () => { } }, {}, { timeoutMs: WIPE_PUSH_TIMEOUT_MS, isFinal: true });
27
+ }
28
+ }
29
+ catch {
30
+ }
31
+ db.close();
32
+ for (const file of [dbFile, `${dbFile}-wal`, `${dbFile}-shm`])
33
+ rmSync(file, { force: true });
34
+ }
@@ -0,0 +1,113 @@
1
+ import { SHARED_TABLES, sharedTable } from "./manifest.js";
2
+ import { dropEntry, entryFor } from "./outbox.js";
3
+ import { UNBOUND_PROJECT_PREFIX } from "../records/recordsTable.js";
4
+ import { UNRESOLVED_PROJECT_ID } from "../local_store/routing.js";
5
+ export function withApplying(db, fn) {
6
+ db.exec("BEGIN IMMEDIATE");
7
+ try {
8
+ db.prepare("INSERT OR REPLACE INTO sync_state (name, value) VALUES ('applying', '1')").run();
9
+ const result = fn();
10
+ db.prepare("DELETE FROM sync_state WHERE name = 'applying'").run();
11
+ db.exec("COMMIT");
12
+ return result;
13
+ }
14
+ catch (error) {
15
+ db.exec("ROLLBACK");
16
+ throw error;
17
+ }
18
+ }
19
+ const columnCache = new WeakMap();
20
+ function columnsOf(db, table) {
21
+ let tables = columnCache.get(db);
22
+ if (!tables) {
23
+ tables = new Map();
24
+ columnCache.set(db, tables);
25
+ }
26
+ let columns = tables.get(table);
27
+ if (!columns) {
28
+ columns = new Set(db.prepare(`PRAGMA table_info("${table}")`).all().map((column) => column.name));
29
+ tables.set(table, columns);
30
+ }
31
+ return columns;
32
+ }
33
+ export function applyFinalRow(db, tableName, row) {
34
+ const table = sharedTable(tableName);
35
+ if (!table)
36
+ return;
37
+ const key = row[table.key];
38
+ if (key === null || key === undefined)
39
+ return;
40
+ const pending = entryFor(db, table.name, String(key));
41
+ if (pending?.op === "delete")
42
+ return;
43
+ const known = columnsOf(db, table.name);
44
+ let kept = null;
45
+ if (pending) {
46
+ const local = db.prepare(`SELECT * FROM "${table.name}" WHERE "${table.key}" = ?`).get(key);
47
+ if (local)
48
+ kept = Object.fromEntries(pending.changed.filter((name) => known.has(name) && name !== table.key).map((name) => [name, local[name] ?? null]));
49
+ }
50
+ const names = Object.keys(row).filter((name) => known.has(name));
51
+ db.prepare(`INSERT OR REPLACE INTO "${table.name}" (${names.map((name) => `"${name}"`).join(", ")}) VALUES (${names.map(() => "?").join(", ")})`).run(...names.map((name) => row[name] ?? null));
52
+ if (pending && kept && Object.keys(kept).length > 0) {
53
+ const assignments = Object.keys(kept).map((name) => `"${name}" = ?`).join(", ");
54
+ db.prepare(`UPDATE "${table.name}" SET ${assignments} WHERE "${table.key}" = ?`).run(...Object.values(kept), key);
55
+ }
56
+ if (pending && typeof row.row_version === "number") {
57
+ db.prepare("UPDATE sync_outbox SET base_version = ? WHERE table_name = ? AND row_key = ?").run(row.row_version, table.name, String(key));
58
+ }
59
+ }
60
+ export function applyTombstone(db, tombstone) {
61
+ const table = sharedTable(tombstone.table);
62
+ if (!table)
63
+ return;
64
+ db.prepare(`DELETE FROM "${table.name}" WHERE "${table.key}" = ?`).run(tombstone.key);
65
+ dropEntry(db, table.name, tombstone.key);
66
+ if (table.name === "projects")
67
+ deleteProjectRows(db, tombstone.key);
68
+ }
69
+ function deleteProjectRows(db, projectId) {
70
+ for (const table of SHARED_TABLES) {
71
+ if (!columnsOf(db, table.name).has("project_id"))
72
+ continue;
73
+ db.prepare(`DELETE FROM sync_outbox WHERE table_name = ? AND row_key IN (SELECT "${table.key}" FROM "${table.name}" WHERE project_id = ?)`).run(table.name, projectId);
74
+ db.prepare(`DELETE FROM "${table.name}" WHERE project_id = ?`).run(projectId);
75
+ }
76
+ }
77
+ const IS_PLACEHOLDER = `(project_id = '${UNRESOLVED_PROJECT_ID}' OR project_id LIKE '${UNBOUND_PROJECT_PREFIX}%')`;
78
+ export function applyScope(db, scope) {
79
+ const organizations = JSON.stringify(scope.organizationIds);
80
+ const projects = JSON.stringify(scope.projectIds);
81
+ const inOrganizations = "(SELECT value FROM json_each(:organizations))";
82
+ const inProjects = "(SELECT value FROM json_each(:projects))";
83
+ const outside = {
84
+ organizations: `id NOT IN ${inOrganizations}`,
85
+ organization_user_map: `organization_id NOT IN ${inOrganizations}`,
86
+ projects: `id NOT IN ${inProjects}`,
87
+ sessions: `organization_id NOT IN ${inOrganizations} OR (project_id IS NOT NULL AND project_id NOT IN ${inProjects})`,
88
+ github_repositories: `NOT EXISTS (SELECT 1 FROM projects p WHERE p.github_installation_id = github_repositories.installation_id
89
+ AND p.github_repo_full_name = github_repositories.full_name)`,
90
+ github_items: "repository_id NOT IN (SELECT repository_id FROM github_repositories)",
91
+ };
92
+ for (const table of SHARED_TABLES) {
93
+ let clause = outside[table.name];
94
+ if (!clause && columnsOf(db, table.name).has("project_id"))
95
+ clause = `project_id NOT IN ${inProjects} AND NOT ${IS_PLACEHOLDER}`;
96
+ if (!clause)
97
+ continue;
98
+ const parameters = {};
99
+ if (clause.includes(":organizations"))
100
+ parameters.organizations = organizations;
101
+ if (clause.includes(":projects"))
102
+ parameters.projects = projects;
103
+ db.prepare(`DELETE FROM sync_outbox WHERE table_name = '${table.name}' AND row_key IN (SELECT "${table.key}" FROM "${table.name}" WHERE ${clause})`).run(parameters);
104
+ db.prepare(`DELETE FROM "${table.name}" WHERE ${clause}`).run(parameters);
105
+ }
106
+ }
107
+ export function readSyncState(db, name) {
108
+ const row = db.prepare("SELECT value FROM sync_state WHERE name = ?").get(name);
109
+ return row?.value ?? null;
110
+ }
111
+ export function writeSyncState(db, name, value) {
112
+ db.prepare("INSERT OR REPLACE INTO sync_state (name, value) VALUES (?, ?)").run(name, value);
113
+ }
@@ -0,0 +1,327 @@
1
+ import { existsSync } from "node:fs";
2
+ import { ensureAccessToken } from "../auth_grant.js";
3
+ import { UNRESOLVED_PROJECT_ID } from "../local_store/routing.js";
4
+ import { assertCredentialOrigin, resolveNuStackUrl } from "../nustack_client.js";
5
+ import { isOriginMismatch } from "../origin_guard.js";
6
+ import { resolveProjectId } from "../project_context.js";
7
+ import { DEVICE_TOKEN_VARIABLE, readVariable } from "../records/environment.js";
8
+ import { RECORD_SCHEMA_VERSION } from "../records/kinds.js";
9
+ import { validateEnvelope } from "../records/recordValidator.js";
10
+ import { REFUSED_ERROR_PREFIX, rekeyRecord, rootOfUnbound, selectUnboundRecords, storedPlacementOf } from "../records/recordsTable.js";
11
+ import { SESSION_SPOOL_RETENTION_MS, listSpools, spoolOf } from "../sessions/sessionEvents.js";
12
+ import { cliVersion } from "../store/open.js";
13
+ import { UPGRADE_COMMAND } from "../store/wipe.js";
14
+ import { applyFinalRow, applyScope, applyTombstone, readSyncState, withApplying, writeSyncState } from "./apply.js";
15
+ import { CLIENT_VERSION_HEADER, PUSH_BATCH_CAP, sharedTable } from "./manifest.js";
16
+ import { backoffEntry, deferEntry, deltaOf, dropEntry, entryFor, selectDueEntries, settleEntry } from "./outbox.js";
17
+ const REQUEST_TIMEOUT_MS = 10_000;
18
+ const MAX_PUSH_PASSES = 50;
19
+ const MAX_PULL_PAGES = 500;
20
+ const DEVICE_PROJECTION_FIELDS = ["origin", "type", "subject", "state", "outcome", "ownerAgentDefinitionId"];
21
+ const outdatedMessage = (message) => `${message} — this CLI (${cliVersion()}) is behind the server. Update NuStack: ${UPGRADE_COMMAND}`;
22
+ async function request(url, init, bearer, timeoutMs) {
23
+ try {
24
+ await assertCredentialOrigin(url);
25
+ }
26
+ catch (error) {
27
+ const code = typeof error.code === "string" ? error.code : null;
28
+ return { ok: false, status: 0, code, message: error instanceof Error ? error.message : String(error) };
29
+ }
30
+ let response;
31
+ try {
32
+ response = await fetch(url, {
33
+ method: init.method,
34
+ headers: {
35
+ ...(init.body !== undefined ? { "content-type": "application/json" } : {}),
36
+ authorization: `Bearer ${bearer}`,
37
+ [CLIENT_VERSION_HEADER]: cliVersion(),
38
+ },
39
+ ...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
40
+ signal: AbortSignal.timeout(timeoutMs),
41
+ });
42
+ }
43
+ catch (error) {
44
+ return { ok: false, status: 0, code: null, message: `the server did not answer: ${error instanceof Error ? error.message : String(error)}` };
45
+ }
46
+ const body = (await response.json().catch(() => null));
47
+ if (!response.ok) {
48
+ const code = typeof body?.error?.code === "string" ? body.error.code : null;
49
+ const detail = typeof body?.error?.message === "string" ? body.error.message : typeof body?.message === "string" ? body.message : `HTTP ${response.status}`;
50
+ return { ok: false, status: response.status, code, message: `${code ?? `HTTP ${response.status}`}: ${detail}` };
51
+ }
52
+ if (!body || body.data === undefined || body.data === null)
53
+ return { ok: false, status: response.status, code: null, message: "the server answered no data" };
54
+ return { ok: true, data: body.data };
55
+ }
56
+ function personBearer(summary) {
57
+ let cached;
58
+ return () => {
59
+ cached ??= ensureAccessToken().then((auth) => auth?.accessToken ?? null, (error) => {
60
+ if (isOriginMismatch(error))
61
+ summary.originRefusal = error.message;
62
+ return null;
63
+ });
64
+ return cached;
65
+ };
66
+ }
67
+ function syncBase(globals, summary) {
68
+ try {
69
+ return resolveNuStackUrl(globals);
70
+ }
71
+ catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ if (isOriginMismatch(error))
74
+ summary.originRefusal = message;
75
+ summary.lastError = message;
76
+ return null;
77
+ }
78
+ }
79
+ async function resolveUnboundRecords(store, dueBy, now) {
80
+ for (const record of selectUnboundRecords(store.db)) {
81
+ const entry = entryFor(store.db, "harness_records", record.id);
82
+ if (!entry || (entry.nextAttemptAt !== null && entry.nextAttemptAt > dueBy))
83
+ continue;
84
+ const root = rootOfUnbound(record.projectId);
85
+ const project = root !== null && existsSync(root) ? await resolveProjectId(root, {}) : null;
86
+ if (project?.ok)
87
+ rekeyRecord(store, record, project.projectId);
88
+ else
89
+ deferEntry(store.db, entry, now);
90
+ }
91
+ }
92
+ function resolveLabelRows(store, dueBy, now) {
93
+ const rows = store.db
94
+ .prepare(`SELECT l.id, l.session_id, l.created_at FROM session_label_rows l
95
+ JOIN sync_outbox o ON o.table_name = 'session_label_rows' AND o.row_key = l.id
96
+ WHERE l.project_id = ? AND (o.next_attempt_at IS NULL OR o.next_attempt_at <= ?)`)
97
+ .all(UNRESOLVED_PROJECT_ID, dueBy);
98
+ if (rows.length === 0)
99
+ return;
100
+ const spools = listSpools();
101
+ for (const row of rows) {
102
+ const spool = spoolOf(row.session_id, spools);
103
+ if (spool) {
104
+ withApplying(store.db, () => store.db.prepare("UPDATE session_label_rows SET session_id = ?, project_id = ? WHERE id = ?").run(spool.rowId, spool.projectId, row.id));
105
+ continue;
106
+ }
107
+ if (now - Date.parse(row.created_at) > SESSION_SPOOL_RETENTION_MS)
108
+ dropEntry(store.db, "session_label_rows", row.id);
109
+ else {
110
+ const entry = entryFor(store.db, "session_label_rows", row.id);
111
+ if (entry)
112
+ deferEntry(store.db, entry, now);
113
+ }
114
+ }
115
+ }
116
+ function isHeld(store, entry) {
117
+ if (entry.op === "delete")
118
+ return false;
119
+ const table = entry.tableName === "harness_records" || entry.tableName === "session_label_rows" ? entry.tableName : null;
120
+ if (!table)
121
+ return false;
122
+ const row = store.db.prepare(`SELECT project_id FROM "${table}" WHERE id = ?`).get(entry.rowKey);
123
+ return !!row && (row.project_id === UNRESOLVED_PROJECT_ID || rootOfUnbound(row.project_id) !== null);
124
+ }
125
+ function isAgentRecord(store, entry) {
126
+ if (entry.tableName !== "harness_records")
127
+ return false;
128
+ const row = store.db.prepare("SELECT written_by_kind FROM harness_records WHERE id = ?").get(entry.rowKey);
129
+ return row?.written_by_kind === "agent";
130
+ }
131
+ function projectDelta(store, delta) {
132
+ if (delta.table !== "harness_records" || typeof delta.columns.payload_json !== "string")
133
+ return delta;
134
+ const row = store.db.prepare("SELECT kind FROM harness_records WHERE id = ?").get(delta.key);
135
+ if (row?.kind !== "obligation" || storedPlacementOf(store.db, delta.key) !== "device")
136
+ return delta;
137
+ const payload = JSON.parse(delta.columns.payload_json);
138
+ const projection = {};
139
+ for (const field of DEVICE_PROJECTION_FIELDS)
140
+ if (payload[field] !== undefined)
141
+ projection[field] = payload[field];
142
+ return { ...delta, columns: { ...delta.columns, payload_json: JSON.stringify(projection) } };
143
+ }
144
+ function recordRefusal(store, delta) {
145
+ if (delta.table !== "harness_records" || delta.op !== "upsert")
146
+ return null;
147
+ const row = store.db.prepare("SELECT * FROM harness_records WHERE id = ?").get(delta.key);
148
+ if (!row)
149
+ return null;
150
+ const payloadText = typeof delta.columns.payload_json === "string" ? delta.columns.payload_json : String(row.payload_json);
151
+ let payload;
152
+ try {
153
+ payload = JSON.parse(payloadText);
154
+ }
155
+ catch {
156
+ return `${REFUSED_ERROR_PREFIX}VALIDATION_FAILED: the payload is not JSON`;
157
+ }
158
+ const envelope = {
159
+ kind: row.kind,
160
+ schemaVersion: RECORD_SCHEMA_VERSION,
161
+ projectId: row.project_id,
162
+ ...(row.session_id !== null ? { sessionId: row.session_id } : {}),
163
+ ...(row.plan_anchor !== null ? { planAnchor: row.plan_anchor } : {}),
164
+ naturalKey: row.natural_key,
165
+ payload,
166
+ occurredAt: row.occurred_at,
167
+ };
168
+ const verdict = validateEnvelope(envelope);
169
+ return verdict.isValid ? null : `${REFUSED_ERROR_PREFIX}VALIDATION_FAILED: ${verdict.errors[0] ?? "the row fails the records schema"}`;
170
+ }
171
+ export function pushDeltas(base, deltas, bearer, timeoutMs = REQUEST_TIMEOUT_MS) {
172
+ return request(`${base}/api/v2/sync/push`, { method: "POST", body: { deltas } }, bearer, timeoutMs);
173
+ }
174
+ async function pushBatch(store, base, batch, bearer, summary, timeoutMs) {
175
+ const answer = await pushDeltas(base, batch.map((item) => item.delta), bearer, timeoutMs);
176
+ const now = Date.now();
177
+ if (!answer.ok) {
178
+ if (answer.status === 426) {
179
+ summary.outdated = outdatedMessage(answer.message);
180
+ summary.lastError = summary.outdated;
181
+ return false;
182
+ }
183
+ for (const item of batch)
184
+ backoffEntry(store.db, item.entry, answer.message, now);
185
+ summary.lastError = answer.message;
186
+ return true;
187
+ }
188
+ for (const refusal of answer.data.refused ?? []) {
189
+ const item = batch[refusal.index];
190
+ if (!item)
191
+ continue;
192
+ const error = `${REFUSED_ERROR_PREFIX}${refusal.code}: ${refusal.message}`;
193
+ backoffEntry(store.db, item.entry, error, now);
194
+ summary.refused += 1;
195
+ summary.lastError = error;
196
+ }
197
+ const accepted = answer.data.rows ?? [];
198
+ withApplying(store.db, () => {
199
+ for (const result of accepted) {
200
+ const item = batch[result.index];
201
+ if (!item)
202
+ continue;
203
+ settleEntry(store.db, item.entry);
204
+ const table = sharedTable(result.table);
205
+ if (!result.row || !table)
206
+ continue;
207
+ if (String(result.row[table.key]) !== result.key)
208
+ store.db.prepare(`DELETE FROM "${table.name}" WHERE "${table.key}" = ?`).run(result.key);
209
+ applyFinalRow(store.db, result.table, result.row);
210
+ }
211
+ });
212
+ summary.sent += accepted.length;
213
+ return true;
214
+ }
215
+ async function pushPass(store, base, summary, dueBy, person, timeoutMs) {
216
+ const now = Date.now();
217
+ await resolveUnboundRecords(store, dueBy, now);
218
+ resolveLabelRows(store, dueBy, now);
219
+ const deviceToken = readVariable(DEVICE_TOKEN_VARIABLE);
220
+ const groups = { device: [], person: [] };
221
+ let taken = 0;
222
+ for (const entry of selectDueEntries(store.db, dueBy)) {
223
+ if (isHeld(store, entry))
224
+ continue;
225
+ const isDevice = isAgentRecord(store, entry);
226
+ if (isDevice && deviceToken === null)
227
+ continue;
228
+ const delta = deltaOf(store.db, entry);
229
+ if (delta === null) {
230
+ dropEntry(store.db, entry.tableName, entry.rowKey);
231
+ continue;
232
+ }
233
+ const projected = projectDelta(store, delta);
234
+ const invalid = recordRefusal(store, projected);
235
+ if (invalid !== null) {
236
+ backoffEntry(store.db, entry, invalid, now);
237
+ summary.refused += 1;
238
+ summary.lastError = invalid;
239
+ continue;
240
+ }
241
+ (isDevice ? groups.device : groups.person).push({ entry, delta: projected });
242
+ taken += 1;
243
+ }
244
+ for (const [credential, list] of Object.entries(groups)) {
245
+ if (list.length === 0)
246
+ continue;
247
+ const bearer = credential === "device" ? deviceToken : await person();
248
+ for (let start = 0; start < list.length; start += PUSH_BATCH_CAP) {
249
+ const batch = list.slice(start, start + PUSH_BATCH_CAP);
250
+ if (bearer === null) {
251
+ const error = summary.originRefusal ?? "this device is signed out";
252
+ for (const item of batch)
253
+ backoffEntry(store.db, item.entry, error, Date.now());
254
+ summary.lastError = error;
255
+ continue;
256
+ }
257
+ if (!(await pushBatch(store, base, batch, bearer, summary, timeoutMs)))
258
+ return 0;
259
+ }
260
+ }
261
+ return taken;
262
+ }
263
+ export async function pushOutbox(store, globals, options = {}) {
264
+ const summary = { sent: 0, refused: 0, lastError: null, originRefusal: null, outdated: null };
265
+ const base = syncBase(globals, summary);
266
+ if (base === null)
267
+ return summary;
268
+ const person = personBearer(summary);
269
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
270
+ const passes = options.isFinal ? 1 : MAX_PUSH_PASSES;
271
+ for (let pass = 0; pass < passes; pass += 1) {
272
+ options.onPass?.();
273
+ const dueBy = pass === 0 && (options.isForeground || options.isFinal) ? Number.MAX_SAFE_INTEGER : Date.now();
274
+ if ((await pushPass(store, base, summary, dueBy, person, timeoutMs)) === 0 || summary.outdated)
275
+ break;
276
+ }
277
+ return summary;
278
+ }
279
+ export function pullCursor(store) {
280
+ return Number(readSyncState(store.db, "cursor") ?? 0);
281
+ }
282
+ function applyPage(store, page) {
283
+ withApplying(store.db, () => {
284
+ for (const pulled of page.rows ?? [])
285
+ applyFinalRow(store.db, pulled.table, pulled.row);
286
+ for (const tombstone of page.tombstones ?? [])
287
+ applyTombstone(store.db, tombstone);
288
+ if (!page.hasMore && page.scope) {
289
+ applyScope(store.db, page.scope);
290
+ writeSyncState(store.db, "scope", JSON.stringify(page.scope));
291
+ }
292
+ writeSyncState(store.db, "cursor", String(page.cursor));
293
+ });
294
+ }
295
+ export async function pullAll(store, globals, options = {}) {
296
+ const summary = { rows: 0, tombstones: 0, cursor: pullCursor(store), isReset: false, lastError: null, originRefusal: null, outdated: null };
297
+ const base = syncBase(globals, summary);
298
+ if (base === null)
299
+ return summary;
300
+ const bearer = await personBearer(summary)();
301
+ if (bearer === null) {
302
+ summary.lastError = summary.originRefusal ?? "this device is signed out";
303
+ return summary;
304
+ }
305
+ for (let page = 0; page < MAX_PULL_PAGES; page += 1) {
306
+ const answer = await request(`${base}/api/v2/sync/pull?cursor=${summary.cursor}`, { method: "GET" }, bearer, options.timeoutMs ?? REQUEST_TIMEOUT_MS);
307
+ if (!answer.ok) {
308
+ if (answer.status === 410 && !summary.isReset) {
309
+ summary.isReset = true;
310
+ summary.cursor = 0;
311
+ withApplying(store.db, () => writeSyncState(store.db, "cursor", "0"));
312
+ continue;
313
+ }
314
+ summary.lastError = answer.status === 426 ? outdatedMessage(answer.message) : answer.message;
315
+ if (answer.status === 426)
316
+ summary.outdated = summary.lastError;
317
+ return summary;
318
+ }
319
+ applyPage(store, answer.data);
320
+ summary.rows += answer.data.rows?.length ?? 0;
321
+ summary.tombstones += answer.data.tombstones?.length ?? 0;
322
+ summary.cursor = answer.data.cursor;
323
+ if (!answer.data.hasMore)
324
+ break;
325
+ }
326
+ return summary;
327
+ }
@@ -0,0 +1,26 @@
1
+ import { resolveNuStackOrigin } from "../nustack_client.js";
2
+ import { resolveProjectId } from "../project_context.js";
3
+ import { readProjectContract } from "../project_contract.js";
4
+ import { originOf } from "../origin_guard.js";
5
+ export async function resolveDoor(root, globals, projectFlag) {
6
+ const project = await resolveProjectId(root, projectFlag ? { project: projectFlag } : {});
7
+ if (!project.ok)
8
+ return { kind: "store", projectId: null, root, reason: "this folder is not bound to a Project" };
9
+ const contract = await readProjectContract(root);
10
+ const door = contract.door;
11
+ if (door === null)
12
+ return { kind: "store", projectId: project.projectId, root, reason: "the harness manifest configures no door" };
13
+ let origin;
14
+ try {
15
+ origin = resolveNuStackOrigin(globals, root).url;
16
+ }
17
+ catch (error) {
18
+ return { kind: "store", projectId: project.projectId, root, reason: error instanceof Error ? error.message : String(error) };
19
+ }
20
+ if (door !== undefined && contract.doorSource === "contract" && originOf(door) !== originOf(origin)) {
21
+ const reason = `the contract's door ${door} is not on the resolved origin ${origin}; nothing is sent`;
22
+ process.stderr.write(`nustack: ${reason}\n`);
23
+ return { kind: "store", projectId: project.projectId, root, reason };
24
+ }
25
+ return { kind: "door", projectId: project.projectId, doorUrl: door ?? origin, isPlatformDefault: door === undefined, root };
26
+ }
@@ -0,0 +1,80 @@
1
+ export const SHARED_TABLES = [
2
+ { name: "organizations", key: "id" },
3
+ { name: "users", key: "id" },
4
+ { name: "organization_user_map", key: "id" },
5
+ { name: "projects", key: "id" },
6
+ { name: "project_local_bindings", key: "id" },
7
+ {
8
+ name: "harness_records",
9
+ key: "id",
10
+ machineColumns: [
11
+ "id",
12
+ "project_id",
13
+ "kind",
14
+ "schema_version",
15
+ "natural_key",
16
+ "idempotency_key",
17
+ "session_id",
18
+ "plan_anchor",
19
+ "payload_json",
20
+ "occurred_at",
21
+ ],
22
+ },
23
+ {
24
+ name: "project_files",
25
+ key: "id",
26
+ machineColumns: [
27
+ "id",
28
+ "project_id",
29
+ "ref",
30
+ "path",
31
+ "type",
32
+ "status",
33
+ "body_sha256",
34
+ "git_blob_sha",
35
+ "body_bytes",
36
+ "git_mode",
37
+ "is_opaque",
38
+ "settlement_id",
39
+ "record_genre",
40
+ "fields_json",
41
+ "record_date",
42
+ "parse_version",
43
+ "commit_sha",
44
+ ],
45
+ },
46
+ {
47
+ name: "project_commits",
48
+ key: "id",
49
+ machineColumns: ["id", "project_id", "ref", "sha", "parent_shas_json", "author_name", "author_email", "subject", "committed_at"],
50
+ },
51
+ { name: "github_repositories", key: "repository_id" },
52
+ { name: "github_items", key: "id" },
53
+ { name: "sessions", key: "id" },
54
+ {
55
+ name: "session_label_rows",
56
+ key: "id",
57
+ machineColumns: [
58
+ "id",
59
+ "session_id",
60
+ "project_id",
61
+ "agent_type",
62
+ "path",
63
+ "estimated_tokens",
64
+ "returned_tokens",
65
+ "handler",
66
+ "check_result",
67
+ "is_bypass_used",
68
+ "latency_ms",
69
+ "tokens_kept_out",
70
+ "created_at",
71
+ ],
72
+ },
73
+ { name: "harness_project_versions", key: "id" },
74
+ ];
75
+ export const PUSH_BATCH_CAP = 200;
76
+ export const CLIENT_VERSION_HEADER = "X-NuStack-Client";
77
+ export const MAIN_REF = "main";
78
+ export function sharedTable(name) {
79
+ return SHARED_TABLES.find((table) => table.name === name) ?? null;
80
+ }