@nurix/nustack 0.23.34 → 0.25.1

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 +6 -6
  47. package/dist/lib/local_store/migrations.js +0 -399
  48. package/dist/lib/records/door.js +0 -79
@@ -1,52 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
- import { chmod, open } from "node:fs/promises";
3
- import path from "node:path";
4
- import { mkdirSecure } from "../secure_file.js";
5
- import { stateDbPath } from "./paths.js";
6
- import { migrateToLatest } from "./migrations.js";
7
- let sqliteModulePromise;
8
- async function loadSqlite() {
9
- if (!sqliteModulePromise) {
10
- sqliteModulePromise = importSqliteQuietly();
11
- }
12
- return sqliteModulePromise;
13
- }
14
- async function importSqliteQuietly() {
15
- return withSuppressedSqliteWarning(() => import("node:sqlite"));
16
- }
17
- export async function withSuppressedSqliteWarning(fn) {
18
- const original = process.emitWarning;
19
- const filtered = (warning, ...args) => {
20
- const name = warning instanceof Error
21
- ? warning.name
22
- : typeof args[0] === "string"
23
- ? args[0]
24
- : args[0]?.type;
25
- const message = warning instanceof Error ? warning.message : String(warning);
26
- if (name === "ExperimentalWarning" && /sqlite/i.test(message))
27
- return;
28
- original(warning, ...args);
29
- };
30
- process.emitWarning = filtered;
31
- try {
32
- return await fn();
33
- }
34
- finally {
35
- process.emitWarning = original;
36
- }
37
- }
38
- export async function openLocalStore(options = {}) {
39
- const dbFile = options.dbPath ?? stateDbPath();
40
- await mkdirSecure(path.dirname(dbFile));
41
- const db = await withSuppressedSqliteWarning(async () => {
42
- const { DatabaseSync } = await loadSqlite();
43
- return new DatabaseSync(dbFile);
44
- });
45
- db.exec("PRAGMA busy_timeout = 5000");
46
- db.exec("PRAGMA journal_mode = WAL");
47
- db.exec("PRAGMA foreign_keys = ON");
48
- migrateToLatest(db);
49
- await chmod(dbFile, 0o600).catch(() => { });
2
+ import { machineStorePath, openMachineDatabase, openMachineDatabaseReadOnly, openSqliteFile } from "../store/open.js";
3
+ function handleOf(db) {
50
4
  return {
51
5
  db,
52
6
  close() {
@@ -54,58 +8,24 @@ export async function openLocalStore(options = {}) {
54
8
  },
55
9
  };
56
10
  }
11
+ export async function openLocalStore(options = {}) {
12
+ return handleOf(await openMachineDatabase(options.dbPath ?? machineStorePath()));
13
+ }
57
14
  export async function openLocalStoreReadOnly(options = {}) {
58
- const dbFile = options.dbPath ?? stateDbPath();
59
- if (!existsSync(dbFile))
60
- return null;
61
- const db = await withSuppressedSqliteWarning(async () => {
62
- const { DatabaseSync } = await loadSqlite();
63
- return new DatabaseSync(dbFile, { readOnly: true });
64
- });
65
- db.exec("PRAGMA busy_timeout = 2000");
66
- const row = db.prepare("PRAGMA user_version").get();
67
- return {
68
- db,
69
- version: Number(row?.user_version ?? 0),
70
- close() {
71
- db.close();
72
- },
73
- };
15
+ const db = await openMachineDatabaseReadOnly(options.dbPath ?? machineStorePath());
16
+ return db ? handleOf(db) : null;
74
17
  }
75
- export async function readStoreVersion(dbFile = stateDbPath()) {
18
+ export async function readStoreVersion(dbFile = machineStorePath()) {
76
19
  if (!existsSync(dbFile))
77
20
  return null;
78
- let db;
79
- try {
80
- db = await withSuppressedSqliteWarning(async () => {
81
- const { DatabaseSync } = await loadSqlite();
82
- return new DatabaseSync(dbFile, { readOnly: true });
83
- });
84
- }
85
- catch {
86
- return headerUserVersion(dbFile);
87
- }
88
21
  try {
89
- const row = db.prepare("PRAGMA user_version").get();
90
- return typeof row?.user_version === "number" ? row.user_version : null;
91
- }
92
- catch {
93
- return headerUserVersion(dbFile);
94
- }
95
- finally {
96
- db.close();
97
- }
98
- }
99
- async function headerUserVersion(dbFile) {
100
- try {
101
- const handle = await open(dbFile, "r");
22
+ const db = await openSqliteFile(dbFile, { readOnly: true });
102
23
  try {
103
- const header = Buffer.alloc(64);
104
- const { bytesRead } = await handle.read(header, 0, 64, 0);
105
- return bytesRead === 64 ? header.readInt32BE(60) : null;
24
+ const row = db.prepare("PRAGMA user_version").get();
25
+ return typeof row?.user_version === "number" ? row.user_version : null;
106
26
  }
107
27
  finally {
108
- await handle.close();
28
+ db.close();
109
29
  }
110
30
  }
111
31
  catch {
@@ -1,4 +1,4 @@
1
- import { LATEST_SCHEMA_VERSION } from "./migrations.js";
1
+ import { cliVersion, storeStampOf } from "../store/open.js";
2
2
  import { withTransaction } from "./database.js";
3
3
  function mapRow(row) {
4
4
  return {
@@ -62,7 +62,7 @@ export function upsertDeviceState(store, patch) {
62
62
  active_organization_id: merged.activeOrganizationId,
63
63
  keychain_service: merged.keychainService,
64
64
  keychain_account: merged.keychainAccount,
65
- schema_version: LATEST_SCHEMA_VERSION,
65
+ schema_version: storeStampOf(cliVersion()),
66
66
  last_auth_refresh_at_ms: merged.lastAuthRefreshAtMs,
67
67
  created_at_ms: merged.createdAtMs,
68
68
  updated_at_ms: now,
@@ -1,6 +1,6 @@
1
1
  export { openLocalStore, withTransaction, } from "./database.js";
2
- export { nustackHomeDir, stateDbPath, locksDir, cacheDir, recoveryDir, ensureNuStackLayout, } from "./paths.js";
3
- export { MIGRATIONS, LATEST_SCHEMA_VERSION, LocalSchemaTooNewError, migrateToLatest, } from "./migrations.js";
2
+ export { machineStorePath } from "../store/open.js";
3
+ export { nustackHomeDir, locksDir, cacheDir, recoveryDir, ensureNuStackLayout, } from "./paths.js";
4
4
  export { INBOUND_APPLICATION_STATES, BINDING_STATUSES, LIVE_BINDING_STATUSES, INBOUND_APPLICATION_TRANSITIONS, isLegalApplicationTransition, } from "./sync_vocabulary.js";
5
5
  export * from "./device_state.js";
6
6
  export * from "./bindings.js";
@@ -4,9 +4,6 @@ import { mkdirSecure } from "../secure_file.js";
4
4
  export function nustackHomeDir() {
5
5
  return path.join(os.homedir(), ".nustack");
6
6
  }
7
- export function stateDbPath() {
8
- return path.join(nustackHomeDir(), "state.db");
9
- }
10
7
  export function locksDir() {
11
8
  return path.join(nustackHomeDir(), "locks");
12
9
  }
@@ -28,9 +25,6 @@ export function sessionEventsDir() {
28
25
  export function briefCachePath(projectId) {
29
26
  return path.join(cacheDir(), "brief", `${projectId}.json`);
30
27
  }
31
- export function obligationsCachePath(projectId, planAnchor) {
32
- return path.join(cacheDir(), "obligations", projectId, `${planAnchor}.json`);
33
- }
34
28
  export function memoryManifestPath(projectId) {
35
29
  return path.join(cacheDir(), "memory-shipped", `${projectId}.json`);
36
30
  }
@@ -13,8 +13,8 @@ export const LABEL_ROW_HANDLERS = [
13
13
  export const LABEL_ROW_CHECK_RESULTS = ["pass", "fail", "budget", "unavailable", "latched"];
14
14
  export const LABEL_ROW_ID_MAX = 128;
15
15
  export const LABEL_ROW_PATH_MAX = 512;
16
- export const LABEL_ROW_SYNC_STATES = ["pending", "synced", "refused", "local"];
17
16
  export const GATED_PATH_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
17
+ export const UNRESOLVED_PROJECT_ID = "";
18
18
  export class LabelRowRefusedError extends Error {
19
19
  code = "LABEL_ROW_REFUSED";
20
20
  constructor(message) {
@@ -38,7 +38,7 @@ function requireInteger(field, value, isSigned) {
38
38
  export function clipLabelRowPath(path) {
39
39
  return path.length <= LABEL_ROW_PATH_MAX ? path : `…${path.slice(path.length - (LABEL_ROW_PATH_MAX - 1))}`;
40
40
  }
41
- export function insertLabelRow(store, row, syncState = "pending") {
41
+ export function insertLabelRow(store, row, isLocalOnly = false) {
42
42
  requireIdentifier("id", row.id);
43
43
  requireIdentifier("session_id", row.session_id);
44
44
  requireIdentifier("agent_type", row.agent_type);
@@ -47,8 +47,6 @@ export function insertLabelRow(store, row, syncState = "pending") {
47
47
  if (row.check_result !== null && !isOneOf(LABEL_ROW_CHECK_RESULTS, row.check_result)) {
48
48
  throw new LabelRowRefusedError(`unknown check result "${String(row.check_result)}"`);
49
49
  }
50
- if (!isOneOf(LABEL_ROW_SYNC_STATES, syncState))
51
- throw new LabelRowRefusedError(`unknown sync state "${String(syncState)}"`);
52
50
  if (row.bypass_used !== 0 && row.bypass_used !== 1)
53
51
  throw new LabelRowRefusedError("bypass_used must be 0 or 1");
54
52
  if (row.path !== null && typeof row.path !== "string")
@@ -59,46 +57,12 @@ export function insertLabelRow(store, row, syncState = "pending") {
59
57
  requireInteger("tokens_kept_out", row.tokens_kept_out, true);
60
58
  requireInteger("created_at", row.created_at, false);
61
59
  store.db
62
- .prepare(`INSERT INTO label_rows (id, session_id, agent_type, path, estimated_tokens, returned_tokens, handler,
63
- check_result, bypass_used, latency_ms, tokens_kept_out, created_at, sync_state)
60
+ .prepare(`INSERT INTO session_label_rows (id, session_id, project_id, agent_type, path, estimated_tokens, returned_tokens,
61
+ handler, check_result, is_bypass_used, latency_ms, tokens_kept_out, created_at)
64
62
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
65
- .run(row.id, row.session_id, row.agent_type, row.path === null ? null : clipLabelRowPath(row.path), row.estimated_tokens, row.returned_tokens, row.handler, row.check_result, row.bypass_used, row.latency_ms, row.tokens_kept_out, row.created_at, syncState);
66
- }
67
- export function labelRowWire(record) {
68
- return {
69
- id: record.id,
70
- sessionId: record.session_id,
71
- agentType: record.agent_type,
72
- path: record.path,
73
- estimatedTokens: record.estimated_tokens,
74
- returnedTokens: record.returned_tokens,
75
- handler: record.handler,
76
- checkResult: record.check_result,
77
- isBypassUsed: record.bypass_used === 1,
78
- latencyMs: record.latency_ms,
79
- tokensKeptOut: record.tokens_kept_out,
80
- createdAt: record.created_at,
81
- };
82
- }
83
- export function listPendingLabelRows(store, limit) {
84
- return store.db
85
- .prepare("SELECT * FROM label_rows WHERE sync_state = 'pending' ORDER BY created_at, id LIMIT ?")
86
- .all(limit);
87
- }
88
- function markPending(store, ids, state) {
89
- if (ids.length === 0)
90
- return 0;
91
- const placeholders = ids.map(() => "?").join(", ");
92
- const result = store.db
93
- .prepare(`UPDATE label_rows SET sync_state = ? WHERE sync_state = 'pending' AND id IN (${placeholders})`)
94
- .run(state, ...ids);
95
- return Number(result.changes);
96
- }
97
- export function markLabelRowsSynced(store, ids) {
98
- return markPending(store, ids, "synced");
99
- }
100
- export function markLabelRowsRefused(store, ids) {
101
- return markPending(store, ids, "refused");
63
+ .run(row.id, row.session_id, UNRESOLVED_PROJECT_ID, row.agent_type, row.path === null ? null : clipLabelRowPath(row.path), row.estimated_tokens, row.returned_tokens, row.handler, row.check_result, row.bypass_used, row.latency_ms, row.tokens_kept_out, new Date(row.created_at).toISOString());
64
+ if (isLocalOnly)
65
+ store.db.prepare("DELETE FROM sync_outbox WHERE table_name = 'session_label_rows' AND row_key = ?").run(row.id);
102
66
  }
103
67
  export function isLatched(store, sessionId, jobKind) {
104
68
  const row = store.db
@@ -153,7 +117,7 @@ export function recordBulkReadOutcome(store, transact, outcome, newId, now = Dat
153
117
  latency_ms: outcome.latencyMs,
154
118
  tokens_kept_out: -entry.returnedTokens,
155
119
  created_at: now,
156
- }, isDirect ? "local" : "pending");
120
+ }, isDirect);
157
121
  if (outcome.result !== "pass")
158
122
  gatePathOnce(store, outcome.sessionId, entry.realPath, now);
159
123
  }
@@ -18,7 +18,7 @@ function significantEntries(dir) {
18
18
  function readMarkerProjectId(root) {
19
19
  const v2 = read(projectJsonPath(root));
20
20
  if (v2 && v2.schemaVersion === 2) {
21
- const id = trimmedField(v2.workspaceId);
21
+ const id = trimmedField(v2.projectId);
22
22
  if (id)
23
23
  return id;
24
24
  }
@@ -42,7 +42,7 @@ function isNestedUnderProject(root) {
42
42
  let dir = path.dirname(root);
43
43
  while (dir && dir !== path.dirname(dir)) {
44
44
  const nustack = path.join(dir, ".nustack");
45
- if (existsSync(path.join(nustack, "workspace.json")) || existsSync(path.join(nustack, "app.json")))
45
+ if (existsSync(path.join(nustack, "project.json")) || existsSync(path.join(nustack, "app.json")))
46
46
  return true;
47
47
  dir = path.dirname(dir);
48
48
  }
@@ -9,7 +9,7 @@ import { classifyFolder } from "./folder_state.js";
9
9
  import { initGitRepository } from "./git_init.js";
10
10
  import { composeOnboardingCommit, resolveHandoffTargets, } from "./handoff_apply.js";
11
11
  import { runRepositoryOnboarding, } from "./binding.js";
12
- export const MANAGED_ONBOARDING_FILES = [".nustack/workspace.json", "nustack.service.json"];
12
+ export const MANAGED_ONBOARDING_FILES = [".nustack/project.json", "nustack.service.json"];
13
13
  export async function initFreshFolder(ctx, options) {
14
14
  const state = classifyFolder(ctx.cwd);
15
15
  if (state.class === "git_worktree") {
@@ -8,7 +8,7 @@ const CURRENT_DOCUMENT_PROTOCOL_VERSION = 1;
8
8
  function buildProjectManifest(identity, existing) {
9
9
  const owned = {
10
10
  schemaVersion: 2,
11
- workspaceId: identity.projectId,
11
+ projectId: identity.projectId,
12
12
  organizationId: identity.organizationId,
13
13
  bindingProtocolVersion: identity.bindingProtocolVersion ?? CURRENT_BINDING_PROTOCOL_VERSION,
14
14
  documentProtocolVersion: identity.documentProtocolVersion ?? CURRENT_DOCUMENT_PROTOCOL_VERSION,
@@ -66,7 +66,7 @@ export async function writeProjectManifests(root, identity, hooks = {}) {
66
66
  }
67
67
  const nextService = {
68
68
  ...serviceManifest,
69
- nustackWorkspace: { v: 2, workspaceId: identity.projectId, organizationId: identity.organizationId },
69
+ nustackProject: { v: 2, projectId: identity.projectId, organizationId: identity.organizationId },
70
70
  toolkit: { name: identity.toolkit.name, version: identity.toolkit.version },
71
71
  };
72
72
  delete nextService.nustackApp;
@@ -137,7 +137,7 @@ async function readProjectJson(root) {
137
137
  return null;
138
138
  return {
139
139
  schemaVersion: 2,
140
- workspaceId: marker.projectId,
140
+ projectId: marker.projectId,
141
141
  organizationId: marker.organizationId,
142
142
  bindingProtocolVersion: typeof raw.bindingProtocolVersion === "number" ? raw.bindingProtocolVersion : 1,
143
143
  documentProtocolVersion: typeof raw.documentProtocolVersion === "number" ? raw.documentProtocolVersion : 1,
@@ -159,10 +159,10 @@ async function readServiceBlocks(root) {
159
159
  return { binding: null, toolkit: null };
160
160
  try {
161
161
  const manifest = JSON.parse(await readFile(filePath, "utf8"));
162
- const v2 = manifest.nustackWorkspace;
162
+ const v2 = manifest.nustackProject;
163
163
  const v1 = manifest.nustackApp;
164
- const binding = typeof v2?.workspaceId === "string" && typeof v2?.organizationId === "string"
165
- ? { projectId: v2.workspaceId, organizationId: v2.organizationId }
164
+ const binding = typeof v2?.projectId === "string" && typeof v2?.organizationId === "string"
165
+ ? { projectId: v2.projectId, organizationId: v2.organizationId }
166
166
  : typeof v1?.appId === "string" && typeof v1?.workspaceId === "string"
167
167
  ? { projectId: v1.appId, organizationId: v1.workspaceId }
168
168
  : null;
@@ -25,7 +25,7 @@ export function originMismatchError(origin, credentialOrigin, credentialKind) {
25
25
  : credentialKind === "none"
26
26
  ? "this machine is not signed in to it, and a sign-in never opens for an origin a repository names"
27
27
  : `this machine's sign-in is for ${credentialOrigin ?? "another origin"}`;
28
- return new HarnessError(HARNESS_CODES.ORIGIN_MISMATCH, `This repository's .nustack/workspace.json names ${origin} as its NuStack origin, but ${held}. ` +
28
+ return new HarnessError(HARNESS_CODES.ORIGIN_MISMATCH, `This repository's .nustack/project.json names ${origin} as its NuStack origin, but ${held}. ` +
29
29
  `To use ${origin}, state it outside the repository: \`nustack login --nustack-url ${origin}\`, or set NUSTACK_URL=${origin}.`);
30
30
  }
31
31
  export function refusedContractOrigin(credentialOrigin, origin) {
@@ -4,9 +4,9 @@ import { existsSync } from "node:fs";
4
4
  import { readManagedValues } from "./env_file.js";
5
5
  import { manifestPath } from "./manifest.js";
6
6
  import { assertSafeOpaqueId } from "./guards.js";
7
- const NO_PROJECT_MESSAGE = "No Project id — run inside a bound repository (`.nustack/workspace.json`), pass `--project <id>`, add an `nustackWorkspace.projectId` to nustack.service.json, or set NUSTACK_WORKSPACE_ID in the managed .env block.";
7
+ const NO_PROJECT_MESSAGE = "No Project id — run inside a bound repository (`.nustack/project.json`), pass `--project <id>`, add an `nustackProject.projectId` to nustack.service.json, or set NUSTACK_PROJECT_ID in the managed .env block.";
8
8
  export function projectJsonPath(cwd) {
9
- return path.join(cwd, ".nustack", "workspace.json");
9
+ return path.join(cwd, ".nustack", "project.json");
10
10
  }
11
11
  export function legacyAppJsonPath(cwd) {
12
12
  return path.join(cwd, ".nustack", "app.json");
@@ -17,7 +17,7 @@ export async function readBindingMarker(cwd) {
17
17
  try {
18
18
  const marker = JSON.parse(await readFile(v2Path, "utf8"));
19
19
  if (marker.schemaVersion === 2) {
20
- const projectId = trimmed(marker.workspaceId);
20
+ const projectId = trimmed(marker.projectId);
21
21
  if (projectId) {
22
22
  return { projectId, organizationId: trimmed(marker.organizationId), version: 2 };
23
23
  }
@@ -54,7 +54,7 @@ async function readManifestProjectId(cwd) {
54
54
  return null;
55
55
  try {
56
56
  const manifest = JSON.parse(await readFile(filePath, "utf8"));
57
- return trimmed(manifest.nustackWorkspace?.workspaceId) ?? trimmed(manifest.nustackApp?.appId);
57
+ return trimmed(manifest.nustackProject?.projectId) ?? trimmed(manifest.nustackApp?.appId);
58
58
  }
59
59
  catch {
60
60
  return null;
@@ -62,7 +62,7 @@ async function readManifestProjectId(cwd) {
62
62
  }
63
63
  async function readEnvProjectId(cwd) {
64
64
  const managed = await readManagedValues(path.join(cwd, ".env"));
65
- const projectId = managed.NUSTACK_WORKSPACE_ID?.trim();
65
+ const projectId = managed.NUSTACK_PROJECT_ID?.trim();
66
66
  return projectId ? projectId : null;
67
67
  }
68
68
  export async function resolveProjectId(cwd, options) {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- export const CONTRACT_PATH = ".nustack/workspace.json";
4
+ export const CONTRACT_PATH = ".nustack/project.json";
5
5
  export const LEGACY_HARNESS_MANIFEST_PATH = ".claude/harness.json";
6
6
  const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
7
7
  function parseObject(text) {
@@ -59,7 +59,7 @@ export function parseProjectContract(contractText, legacyText) {
59
59
  const contractDoor = doorOf(contract);
60
60
  const legacyDoor = doorOf(legacyMount(legacy));
61
61
  return {
62
- projectId: contract === null ? null : nonEmpty(contract.workspaceId),
62
+ projectId: contract === null ? null : nonEmpty(contract.projectId),
63
63
  harness: contract === null ? null : harnessOf(contract.harness),
64
64
  door: contractDoor !== undefined ? contractDoor : legacyDoor,
65
65
  doorSource: contractDoor !== undefined ? "contract" : legacyDoor !== undefined ? "legacy" : null,
@@ -1,160 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, utimesSync, writeSync } from "node:fs";
2
+ import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, utimesSync, writeSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { ensureAccessToken } from "../auth_grant.js";
6
- import { isOriginMismatch } from "../origin_guard.js";
7
5
  import { recordsDrainLockPath } from "../local_store/paths.js";
8
- import { resolveNuStackUrl } from "../nustack_client.js";
9
- import { RECORDS_BATCH_CAP, postRecords, resolveDoor, retryDelayMs } from "./door.js";
10
- import { DEVICE_TOKEN_VARIABLE, DRAIN_CHILD_VARIABLE, NO_DETACHED_DRAIN_VARIABLE, isFlagSet, readVariable } from "./environment.js";
11
- import { envelopeOfStored } from "./recordInput.js";
12
- import { validateEnvelope } from "./recordValidator.js";
13
- import { OBLIGATION_AGENT_OWNED, markBackoff, markDeferred, markRefused, markSynced, pruneSynced, rekeyRecord, selectDueRecords, } from "./recordsTable.js";
6
+ import { DRAIN_CHILD_VARIABLE, NO_DETACHED_DRAIN_VARIABLE, isFlagSet } from "./environment.js";
14
7
  export const DRAIN_LOCK_STALE_MS = 120_000;
15
- const MAX_PASSES = 50;
16
- const DEVICE_PROJECTION_FIELDS = ["origin", "type", "subject", "state", "outcome", "ownerAgentDefinitionId"];
17
- function payloadToSend(row) {
18
- const payload = JSON.parse(row.payloadJson);
19
- if (row.placement !== "device" || row.kind !== "obligation")
20
- return payload;
21
- const projection = {};
22
- for (const field of DEVICE_PROJECTION_FIELDS)
23
- if (payload[field] !== undefined)
24
- projection[field] = payload[field];
25
- return projection;
26
- }
27
- async function targetOf(store, row, globals, doors) {
28
- if (row.doorUrl !== null && row.projectId !== null)
29
- return { row, doorUrl: row.doorUrl, projectId: row.projectId };
30
- if (row.repositoryRoot !== null && existsSync(row.repositoryRoot)) {
31
- let door = doors.get(row.repositoryRoot);
32
- if (!door) {
33
- door = await resolveDoor(row.repositoryRoot, globals);
34
- doors.set(row.repositoryRoot, door);
35
- }
36
- if (door.kind !== "door")
37
- return null;
38
- if (row.projectId !== null)
39
- return { row, doorUrl: door.doorUrl, projectId: row.projectId };
40
- const rekeyed = rekeyRecord(store, row, door.projectId);
41
- return rekeyed ? { row: rekeyed, doorUrl: door.doorUrl, projectId: door.projectId } : "gone";
42
- }
43
- if (row.projectId !== null)
44
- return { row, doorUrl: resolveNuStackUrl(globals), projectId: row.projectId };
45
- return null;
46
- }
47
- function isRowRefusal(outcome) {
48
- return outcome.status === 400 || (outcome.status === 403 && outcome.code === OBLIGATION_AGENT_OWNED);
49
- }
50
- async function drainPass(store, globals, result, shouldIgnoreBackoff) {
51
- const now = Date.now();
52
- const deviceToken = readVariable(DEVICE_TOKEN_VARIABLE);
53
- const due = selectDueRecords(store, shouldIgnoreBackoff ? Number.MAX_SAFE_INTEGER : now, deviceToken !== null);
54
- if (due.length === 0)
55
- return 0;
56
- const doors = new Map();
57
- const groups = new Map();
58
- for (const selected of due) {
59
- const target = await targetOf(store, selected, globals, doors);
60
- if (target === "gone")
61
- continue;
62
- if (target === null) {
63
- markDeferred(store, selected, now + retryDelayMs(Math.max(1, selected.attempts)));
64
- continue;
65
- }
66
- const envelope = envelopeOfStored(target.row, target.projectId, payloadToSend(target.row));
67
- const verdict = validateEnvelope(envelope);
68
- if (!verdict.isValid) {
69
- const error = `VALIDATION_FAILED: ${verdict.errors[0] ?? "the row fails the records schema"}`;
70
- if (markRefused(store, target.row, error)) {
71
- result.refused += 1;
72
- result.lastError = error;
73
- }
74
- continue;
75
- }
76
- const group = `${target.row.credential ?? ""}\u0000${target.doorUrl}\u0000${target.projectId}\u0000${target.row.kind}`;
77
- const list = groups.get(group) ?? [];
78
- list.push({ row: target.row, envelope, doorUrl: target.doorUrl, projectId: target.projectId });
79
- groups.set(group, list);
80
- }
81
- let personToken;
82
- for (const list of groups.values()) {
83
- const first = list[0];
84
- let bearer;
85
- if (first.row.credential === "device")
86
- bearer = deviceToken;
87
- else {
88
- if (personToken === undefined) {
89
- personToken = await ensureAccessToken().then((auth) => auth?.accessToken ?? null, (error) => {
90
- if (isOriginMismatch(error))
91
- result.originRefusal = error.message;
92
- return null;
93
- });
94
- }
95
- bearer = personToken;
96
- }
97
- for (let start = 0; start < list.length; start += RECORDS_BATCH_CAP) {
98
- const batch = list.slice(start, start + RECORDS_BATCH_CAP);
99
- if (bearer === null) {
100
- backoffAll(store, batch, result.originRefusal ?? "this device is signed out", result);
101
- continue;
102
- }
103
- await sendBatch(store, batch, first.row.kind, bearer, result);
104
- }
105
- }
106
- return due.length;
107
- }
108
- function backoffAll(store, batch, error, result) {
109
- const now = Date.now();
110
- for (const item of batch)
111
- markBackoff(store, item.row, error, now + retryDelayMs(item.row.attempts + 1));
112
- result.lastError = error;
113
- }
114
- async function sendBatch(store, batch, kind, bearer, result) {
115
- const { doorUrl, projectId } = batch[0];
116
- const outcome = await postRecords(doorUrl, projectId, { kind, rows: batch.map((item) => item.envelope) }, bearer);
117
- if (outcome.ok) {
118
- for (const item of batch)
119
- markSynced(store, item.row);
120
- result.sent += batch.length;
121
- return;
122
- }
123
- if (!isRowRefusal(outcome)) {
124
- backoffAll(store, batch, outcome.message, result);
125
- return;
126
- }
127
- if (batch.length === 1) {
128
- if (markRefused(store, batch[0].row, outcome.message))
129
- result.refused += 1;
130
- result.lastError = outcome.message;
131
- return;
132
- }
133
- for (const item of batch) {
134
- const alone = await postRecords(doorUrl, projectId, { kind, rows: [item.envelope] }, bearer);
135
- if (alone.ok) {
136
- markSynced(store, item.row);
137
- result.sent += 1;
138
- }
139
- else if (isRowRefusal(alone)) {
140
- if (markRefused(store, item.row, alone.message))
141
- result.refused += 1;
142
- result.lastError = alone.message;
143
- }
144
- else
145
- backoffAll(store, [item], alone.message, result);
146
- }
147
- }
148
- export async function drainRecords(store, globals, options = {}) {
149
- const result = { sent: 0, refused: 0, lastError: null, originRefusal: null };
150
- pruneSynced(store, Date.now());
151
- for (let pass = 0; pass < MAX_PASSES; pass += 1) {
152
- options.onPass?.();
153
- if ((await drainPass(store, globals, result, pass === 0 && options.isForeground === true)) === 0)
154
- break;
155
- }
156
- return result;
157
- }
158
8
  function tryCreateLock(lockPath) {
159
9
  try {
160
10
  const fd = openSync(lockPath, "wx", 0o600);
@@ -211,7 +61,7 @@ export function startDetachedDrain(options = {}) {
211
61
  try {
212
62
  if (isFlagSet(NO_DETACHED_DRAIN_VARIABLE) || isFlagSet(DRAIN_CHILD_VARIABLE))
213
63
  return;
214
- const args = [cliEntryPath(), ...(options.nustackUrl ? ["--nustack-url", options.nustackUrl] : []), "records", "sync", "--quiet"];
64
+ const args = [cliEntryPath(), ...(options.nustackUrl ? ["--nustack-url", options.nustackUrl] : []), "store", "sync", "--quiet"];
215
65
  const child = spawn(process.execPath, args, {
216
66
  detached: true,
217
67
  stdio: "ignore",
@@ -0,0 +1,16 @@
1
+ export const RECORD_KINDS = ["obligation", "gate_event", "changelog", "memory", "backlog", "proposal", "triage"];
2
+ export const LEGACY_KIND_TRANSLATION = { "backlog.remove": "backlog", bookkeeping: "obligation" };
3
+ export const RECORDS_BATCH_CAP = 200;
4
+ export const RECORD_SCHEMA_VERSION = 1;
5
+ export function isRecordKind(value) {
6
+ return typeof value === "string" && RECORD_KINDS.includes(value);
7
+ }
8
+ export function isLegacyKind(value) {
9
+ return typeof value === "string" && Object.hasOwn(LEGACY_KIND_TRANSLATION, value);
10
+ }
11
+ export function isEmitKind(value) {
12
+ return isRecordKind(value) || isLegacyKind(value);
13
+ }
14
+ export function translatedKindOf(kind) {
15
+ return isLegacyKind(kind) ? LEGACY_KIND_TRANSLATION[kind] : kind;
16
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { RECORD_SCHEMA_VERSION, isLegacyKind, translatedKindOf } from "./door.js";
2
+ import { RECORD_SCHEMA_VERSION, isLegacyKind, translatedKindOf } from "./kinds.js";
3
3
  import { idempotencyKeyOf, naturalKeyOf } from "./naturalKey.js";
4
4
  import { validateEnvelope } from "./recordValidator.js";
5
5
  import { scanPayload } from "./secretScan.js";