@north-light/api 0.3.561 → 0.3.562

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 (87) hide show
  1. package/dist/api/client.d.ts +27 -4
  2. package/dist/api/client.js +53 -0
  3. package/dist/api/contract.d.ts +1 -1
  4. package/dist/api/contract.js +1 -1
  5. package/dist/api/dto/bash-jobs.d.ts +5 -1
  6. package/dist/api/dto/broker-ops.d.ts +25 -1
  7. package/dist/api/dto/charges.d.ts +9 -2
  8. package/dist/api/dto/delivery.d.ts +26 -2
  9. package/dist/api/dto/execution-start.d.ts +57 -0
  10. package/dist/api/dto/execution-start.js +2 -0
  11. package/dist/api/dto/modelauth.d.ts +5 -0
  12. package/dist/api/dto/node-records.d.ts +16 -0
  13. package/dist/api/dto/objects.d.ts +1 -1
  14. package/dist/api/dto/reports.d.ts +0 -3
  15. package/dist/api/dto/run-events.d.ts +7 -1
  16. package/dist/api/dto/settings.d.ts +55 -0
  17. package/dist/api/dto/settings.js +4 -0
  18. package/dist/api/index.d.ts +2 -0
  19. package/dist/api/index.js +2 -0
  20. package/dist/api/routes.d.ts +11 -0
  21. package/dist/api/routes.js +12 -0
  22. package/dist/core/canvas/paths.d.ts +0 -1
  23. package/dist/core/canvas/paths.js +0 -3
  24. package/dist/core/canvas/person-state.d.ts +26 -0
  25. package/dist/core/canvas/person-state.js +41 -0
  26. package/dist/core/command-plugins/bundle.d.ts +1 -1
  27. package/dist/core/command-plugins/bundle.js +5 -5
  28. package/dist/core/config.d.ts +16 -15
  29. package/dist/core/config.js +99 -76
  30. package/dist/core/conversation-store/client.d.ts +2 -0
  31. package/dist/core/conversation-store/client.js +111 -0
  32. package/dist/core/conversation-store/index.d.ts +96 -0
  33. package/dist/core/conversation-store/index.js +62 -0
  34. package/dist/core/conversation-store/patch.d.ts +3 -0
  35. package/dist/core/conversation-store/patch.js +192 -0
  36. package/dist/core/conversation-store/repair.d.ts +45 -0
  37. package/dist/core/conversation-store/repair.js +84 -0
  38. package/dist/core/env-name.d.ts +1 -0
  39. package/dist/core/env-name.js +16 -0
  40. package/dist/core/events/emit.js +7 -5
  41. package/dist/core/git.d.ts +28 -0
  42. package/dist/core/git.js +103 -0
  43. package/dist/core/installed-plugins.js +12 -7
  44. package/dist/core/managed-providers.d.ts +72 -0
  45. package/dist/core/managed-providers.js +61 -0
  46. package/dist/core/plugin-env.js +1 -2
  47. package/dist/core/profiles/manifest.d.ts +14 -18
  48. package/dist/core/profiles/manifest.js +79 -98
  49. package/dist/core/runtime/broker/attachments-fetch.d.ts +5 -0
  50. package/dist/core/runtime/broker/attachments-fetch.js +20 -0
  51. package/dist/core/runtime/broker/daemon-ops.d.ts +10 -1
  52. package/dist/core/runtime/broker/daemon-ops.js +41 -3
  53. package/dist/core/runtime/broker/node-token.d.ts +3 -0
  54. package/dist/core/runtime/broker/node-token.js +16 -0
  55. package/dist/core/runtime/broker/signal-stream.js +2 -2
  56. package/dist/core/scoped-state/materialized-plugins.d.ts +18 -0
  57. package/dist/core/scoped-state/materialized-plugins.js +103 -0
  58. package/dist/core/scoped-state/person-store.d.ts +72 -0
  59. package/dist/core/scoped-state/person-store.js +112 -0
  60. package/dist/core/user-settings.js +1 -1
  61. package/dist/daemon/broker-signals/bus.d.ts +48 -11
  62. package/dist/daemon/broker-signals/bus.js +116 -46
  63. package/dist/shared/inbox-entry-body.js +2 -2
  64. package/dist/types.d.ts +12 -0
  65. package/package.json +1 -1
  66. package/dist/core/canvas/legacy-person.d.ts +0 -4
  67. package/dist/core/canvas/legacy-person.js +0 -4
  68. package/dist/core/locked-file.d.ts +0 -27
  69. package/dist/core/locked-file.js +0 -121
  70. package/dist/core/profiles/env-store.d.ts +0 -22
  71. package/dist/core/profiles/env-store.js +0 -166
  72. package/dist/core/scoped-state/db.d.ts +0 -17
  73. package/dist/core/scoped-state/db.js +0 -261
  74. package/dist/core/scoped-state/migrate.d.ts +0 -8
  75. package/dist/core/scoped-state/migrate.js +0 -187
  76. package/dist/core/scoped-state/profiles.d.ts +0 -27
  77. package/dist/core/scoped-state/profiles.js +0 -94
  78. package/dist/core/scoped-state/providers.d.ts +0 -24
  79. package/dist/core/scoped-state/providers.js +0 -19
  80. package/dist/core/scoped-state/schema.d.ts +0 -10
  81. package/dist/core/scoped-state/schema.js +0 -47
  82. package/dist/core/scoped-state/settings.d.ts +0 -28
  83. package/dist/core/scoped-state/settings.js +0 -104
  84. package/dist/core/sqlite-statements.d.ts +0 -4
  85. package/dist/core/sqlite-statements.js +0 -17
  86. package/dist/core/subscription-state.d.ts +0 -129
  87. package/dist/core/subscription-state.js +0 -299
@@ -13,6 +13,8 @@ export declare const routes: {
13
13
  readonly reviveAll: () => string;
14
14
  readonly node: (id: string) => string;
15
15
  readonly nodeMemoryReads: (id: string) => string;
16
+ readonly executionStart: (id: string) => string;
17
+ readonly environmentGitCredential: (id: string) => string;
16
18
  readonly nodeOutcome: (id: string) => string;
17
19
  readonly nodeOutcomeDelivery: (id: string) => string;
18
20
  readonly nodeEvents: (id: string) => string;
@@ -27,6 +29,7 @@ export declare const routes: {
27
29
  readonly nodeResult: (id: string) => string;
28
30
  readonly nodeJobs: (id: string) => string;
29
31
  readonly nodeJob: (id: string, jobId: string) => string;
32
+ readonly nodeJobReport: (id: string, jobId: string) => string;
30
33
  readonly nodeJobBackground: (id: string, jobId: string) => string;
31
34
  readonly nodeDelivery: (id: string) => string;
32
35
  readonly deliveryDryRun: () => string;
@@ -38,10 +41,14 @@ export declare const routes: {
38
41
  readonly nodeRelaunchRoot: (id: string) => string;
39
42
  readonly nodeBrokerSessionBound: (id: string) => string;
40
43
  readonly nodeBrokerSettle: (id: string) => string;
44
+ readonly nodeBrokerIdle: (id: string) => string;
41
45
  readonly nodeBrokerParkComplete: (id: string) => string;
42
46
  readonly nodeBrokerParkActivity: (id: string) => string;
43
47
  readonly nodeBrokerTelemetry: (id: string) => string;
44
48
  readonly nodeLog: (id: string) => string;
49
+ readonly nodeSummaries: (id: string) => string;
50
+ readonly nodeAttachments: (id: string) => string;
51
+ readonly nodeAttachment: (id: string, sha256: string) => string;
45
52
  readonly nodeTelemetry: (id: string) => string;
46
53
  readonly nodeRecap: (id: string) => string;
47
54
  readonly nodeInboxReport: (id: string) => string;
@@ -52,6 +59,8 @@ export declare const routes: {
52
59
  readonly nodeBrokerModel: (id: string) => string;
53
60
  readonly nodeBrokerExtensionState: (id: string) => string;
54
61
  readonly nodeBrokerSignals: (id: string) => string;
62
+ readonly nodeBrokerStructuredRequest: (id: string) => string;
63
+ readonly nodeBrokerGoal: (id: string) => string;
55
64
  readonly nodeBrokerGeneratedName: (id: string) => string;
56
65
  readonly nodeBrokerPersonaAck: (id: string) => string;
57
66
  readonly brokerTurn: (id: string) => string;
@@ -140,9 +149,11 @@ export declare const routes: {
140
149
  readonly appEnvVar: (variable: string) => string;
141
150
  readonly profileEnv: (name: string) => string;
142
151
  readonly profileEnvVar: (name: string, variable: string) => string;
152
+ readonly settings: () => string;
143
153
  readonly modelAuths: () => string;
144
154
  readonly modelAuthReadiness: () => string;
145
155
  readonly modelAuth: (provider: string) => string;
156
+ readonly modelAuthAccount: (provider: string, label: string) => string;
146
157
  readonly modelAuthFlows: () => string;
147
158
  readonly modelAuthFlow: (flowId: string) => string;
148
159
  readonly modelAuthFlowComplete: (flowId: string) => string;
@@ -28,6 +28,8 @@ export const routes = {
28
28
  reviveAll: () => `${V}/nodes/revive-all`,
29
29
  node: (id) => `${V}/nodes/${id}`,
30
30
  nodeMemoryReads: (id) => `${V}/nodes/${id}/memory-reads`,
31
+ executionStart: (id) => `${V}/executions/${id}/start`,
32
+ environmentGitCredential: (id) => `${V}/environments/${id}/git-credential`,
31
33
  nodeOutcome: (id) => `${V}/nodes/${id}/outcome`,
32
34
  nodeOutcomeDelivery: (id) => `${V}/nodes/${id}/outcome-delivery`,
33
35
  nodeEvents: (id) => `${V}/nodes/${id}/events`,
@@ -43,6 +45,7 @@ export const routes = {
43
45
  nodeResult: (id) => `${V}/nodes/${id}/result`,
44
46
  nodeJobs: (id) => `${V}/nodes/${id}/jobs`,
45
47
  nodeJob: (id, jobId) => `${V}/nodes/${id}/jobs/${jobId}`,
48
+ nodeJobReport: (id, jobId) => `${V}/nodes/${id}/jobs/${jobId}/report`,
46
49
  nodeJobBackground: (id, jobId) => `${V}/nodes/${id}/jobs/${jobId}/background`,
47
50
  nodeDelivery: (id) => `${V}/nodes/${id}/delivery`,
48
51
  deliveryDryRun: () => `${V}/delivery/dry-run`,
@@ -56,10 +59,14 @@ export const routes = {
56
59
  nodeRelaunchRoot: (id) => `${V}/nodes/${id}/relaunch-root`,
57
60
  nodeBrokerSessionBound: (id) => `${V}/nodes/${id}/broker/session-bound`,
58
61
  nodeBrokerSettle: (id) => `${V}/nodes/${id}/broker/settle`,
62
+ nodeBrokerIdle: (id) => `${V}/nodes/${id}/broker/idle`,
59
63
  nodeBrokerParkComplete: (id) => `${V}/nodes/${id}/broker/park-complete`,
60
64
  nodeBrokerParkActivity: (id) => `${V}/nodes/${id}/broker/park-activity`,
61
65
  nodeBrokerTelemetry: (id) => `${V}/nodes/${id}/broker/telemetry`,
62
66
  nodeLog: (id) => `${V}/nodes/${id}/records/log`,
67
+ nodeSummaries: (id) => `${V}/nodes/${id}/records/summaries`,
68
+ nodeAttachments: (id) => `${V}/nodes/${id}/attachments`,
69
+ nodeAttachment: (id, sha256) => `${V}/nodes/${id}/attachments/${sha256}`,
63
70
  nodeTelemetry: (id) => `${V}/nodes/${id}/records/telemetry`,
64
71
  nodeRecap: (id) => `${V}/nodes/${id}/records/recap`,
65
72
  nodeInboxReport: (id) => `${V}/nodes/${id}/records/inbox`,
@@ -70,6 +77,8 @@ export const routes = {
70
77
  nodeBrokerModel: (id) => `${V}/nodes/${id}/broker/model`,
71
78
  nodeBrokerExtensionState: (id) => `${V}/nodes/${id}/broker/extension-state`,
72
79
  nodeBrokerSignals: (id) => `${V}/nodes/${id}/broker/signals`,
80
+ nodeBrokerStructuredRequest: (id) => `${V}/nodes/${id}/broker/structured-request`,
81
+ nodeBrokerGoal: (id) => `${V}/nodes/${id}/broker/goal`,
73
82
  nodeBrokerGeneratedName: (id) => `${V}/nodes/${id}/broker/generated-name`,
74
83
  nodeBrokerPersonaAck: (id) => `${V}/nodes/${id}/broker/persona-ack`,
75
84
  brokerTurn: (id) => `${V}/broker/${id}/turn`,
@@ -168,10 +177,13 @@ export const routes = {
168
177
  appEnvVar: (variable) => `${V}/apps/self/env/${encodeURIComponent(variable)}`,
169
178
  profileEnv: (name) => `${V}/profiles/${name}/env`,
170
179
  profileEnvVar: (name, variable) => `${V}/profiles/${name}/env/${variable}`,
180
+ // Person settings
181
+ settings: () => `${V}/settings`,
171
182
  // Model auth
172
183
  modelAuths: () => `${V}/model-auth`,
173
184
  modelAuthReadiness: () => `${V}/model-auth/readiness`,
174
185
  modelAuth: (provider) => `${V}/model-auth/${provider}`,
186
+ modelAuthAccount: (provider, label) => `${V}/model-auth/${provider}/accounts/${encodeURIComponent(label)}`,
175
187
  modelAuthFlows: () => `${V}/model-auth/flows`,
176
188
  modelAuthFlow: (flowId) => `${V}/model-auth/flows/${encodeURIComponent(flowId)}`,
177
189
  modelAuthFlowComplete: (flowId) => `${V}/model-auth/flows/${encodeURIComponent(flowId)}/complete`,
@@ -71,7 +71,6 @@ export declare function inboxPath(nodeId: string): string;
71
71
  export declare function situationalContextPath(nodeId: string): string;
72
72
  export declare function passivePath(nodeId: string): string;
73
73
  export declare function transcriptPath(nodeId: string): string;
74
- export declare function sessionPtrPath(nodeId: string): string;
75
74
  /** Create the full directory skeleton for a node. Idempotent. The agent and
76
75
  * run folders are app-writable, so their subfolders are made beneath them. */
77
76
  export declare function ensureNodeDirs(nodeId: string, grantee: string): void;
@@ -152,9 +152,6 @@ export function passivePath(nodeId) {
152
152
  export function transcriptPath(nodeId) {
153
153
  return join(nodeDir(nodeId), 'transcript.jsonl');
154
154
  }
155
- export function sessionPtrPath(nodeId) {
156
- return join(nodeDir(nodeId), 'session.ptr');
157
- }
158
155
  /** Create the full directory skeleton for a node. Idempotent. The agent and
159
156
  * run folders are app-writable, so their subfolders are made beneath them. */
160
157
  export function ensureNodeDirs(nodeId, grantee) {
@@ -0,0 +1,26 @@
1
+ /** Thrown by `personId()` before the person is known (a hosted first boot, before its first grant snapshot). */
2
+ export declare class PersonUnresolvedError extends Error {
3
+ readonly code = "person_unresolved";
4
+ constructor();
5
+ }
6
+ /** Thrown by `personId()` on a cloud role (`crtrd --role api|reconcile`) outside a request: the control plane serves many people and
7
+ * has no install person to fall back to. A lane that works for a person takes the person from the row it claimed. */
8
+ export declare class PersonOutsideRequestError extends Error {
9
+ readonly code = "person_outside_request";
10
+ constructor();
11
+ }
12
+ /** crtrd sets this once at boot; it wins unconditionally. */
13
+ export declare function setPersonId(id: string): void;
14
+ /** crtrd calls this at boot on a cloud role, instead of resolving a person: from then on a person read outside a request throws. */
15
+ export declare function markCloudRole(): void;
16
+ /** Whether this process is a cloud role (`markCloudRole` ran at boot). */
17
+ export declare function isCloudRole(): boolean;
18
+ /** Run `fn` as `person` (`user:<id>`, the request token's `sub`): every `personId()` inside it, across awaits, answers `person`. */
19
+ export declare function runAsPerson<T>(person: string, fn: () => T): T;
20
+ /** The request's person; else, on a Mac, the boot-set person. Throws `PersonOutsideRequestError` on a cloud role outside a request and
21
+ * `PersonUnresolvedError` when a Mac's person is not bound. Tests and non-daemon processes use `personIdAsync()`. */
22
+ export declare function personId(): string;
23
+ /** The boot-set person, if any (for `personIdAsync`). */
24
+ export declare function bootPersonId(): string | undefined;
25
+ /** The request's person, if any (for `personIdAsync`). */
26
+ export declare function requestPersonId(): string | undefined;
@@ -0,0 +1,41 @@
1
+ // person-state.ts — the process's person binding (boot-set person, cloud-role flag, per-request person). Pure: no database, so the
2
+ // CLI-reachable settings cache can read the current person without reaching canvas state.
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ /** Thrown by `personId()` before the person is known (a hosted first boot, before its first grant snapshot). */
5
+ export class PersonUnresolvedError extends Error {
6
+ code = 'person_unresolved';
7
+ constructor() { super('the install\'s person is not resolved yet'); this.name = 'PersonUnresolvedError'; }
8
+ }
9
+ /** Thrown by `personId()` on a cloud role (`crtrd --role api|reconcile`) outside a request: the control plane serves many people and
10
+ * has no install person to fall back to. A lane that works for a person takes the person from the row it claimed. */
11
+ export class PersonOutsideRequestError extends Error {
12
+ code = 'person_outside_request';
13
+ constructor() { super('no person: a cloud role reads the person only inside a request, from its token'); this.name = 'PersonOutsideRequestError'; }
14
+ }
15
+ let daemonPersonId;
16
+ let cloudRole = false;
17
+ const requestPersons = new AsyncLocalStorage();
18
+ /** crtrd sets this once at boot; it wins unconditionally. */
19
+ export function setPersonId(id) { daemonPersonId = id; }
20
+ /** crtrd calls this at boot on a cloud role, instead of resolving a person: from then on a person read outside a request throws. */
21
+ export function markCloudRole() { cloudRole = true; }
22
+ /** Whether this process is a cloud role (`markCloudRole` ran at boot). */
23
+ export function isCloudRole() { return cloudRole; }
24
+ /** Run `fn` as `person` (`user:<id>`, the request token's `sub`): every `personId()` inside it, across awaits, answers `person`. */
25
+ export function runAsPerson(person, fn) { return requestPersons.run(person, fn); }
26
+ /** The request's person; else, on a Mac, the boot-set person. Throws `PersonOutsideRequestError` on a cloud role outside a request and
27
+ * `PersonUnresolvedError` when a Mac's person is not bound. Tests and non-daemon processes use `personIdAsync()`. */
28
+ export function personId() {
29
+ const request = requestPersons.getStore();
30
+ if (request !== undefined)
31
+ return request;
32
+ if (cloudRole)
33
+ throw new PersonOutsideRequestError();
34
+ if (daemonPersonId === undefined)
35
+ throw new PersonUnresolvedError();
36
+ return daemonPersonId;
37
+ }
38
+ /** The boot-set person, if any (for `personIdAsync`). */
39
+ export function bootPersonId() { return daemonPersonId; }
40
+ /** The request's person, if any (for `personIdAsync`). */
41
+ export function requestPersonId() { return requestPersons.getStore(); }
@@ -98,7 +98,7 @@ export declare function isInstalledGeneration(name: string, scope: Scope, root:
98
98
  * are recorded either way, because both describe the bytes now at the root. */
99
99
  export declare function recordCheckedGeneration(scope: Scope, name: string, etag: string | undefined, options: {
100
100
  swapped: boolean;
101
- }): void;
101
+ }): Promise<void>;
102
102
  /** Every file lands by temp file plus rename, fsynced, so a crash mid-write leaves the previous bytes or
103
103
  * the whole new ones — never a 0-byte `plugin.json` or `commands.json` that crashes the next boot. */
104
104
  export declare function writeStagedBundle(stagingRoot: string, manifest: PluginManifest, bundle: ValidatedBundle): void;
@@ -474,8 +474,8 @@ export function isInstalledGeneration(name, scope, root, version) {
474
474
  /** The ledger for a check that reached a served archive. `swapped` says whether
475
475
  * that archive replaced the package on disk; the validator and the check time
476
476
  * are recorded either way, because both describe the bytes now at the root. */
477
- export function recordCheckedGeneration(scope, name, etag, options) {
478
- updateState(scope, (state) => {
477
+ export async function recordCheckedGeneration(scope, name, etag, options) {
478
+ await updateState(scope, (state) => {
479
479
  if (state.plugins[name] === undefined)
480
480
  state.plugins[name] = {};
481
481
  if (options.swapped)
@@ -598,10 +598,10 @@ export async function installBundleArchive(name, raw, options) {
598
598
  // rewritten. The forbidden state — a validator for a generation the disk does
599
599
  // not have, and so a permanent 304 against a package that was never unpacked
600
600
  // — is unreachable from this order.
601
- ensureScopeInitialized(scope, scopeRootPath);
602
- updateConfig(scope, (cfg) => {
601
+ await ensureScopeInitialized(scope, scopeRootPath);
602
+ await updateConfig(scope, (cfg) => {
603
603
  cfg.plugins[name] = { enabled: options.enable ? true : (cfg.plugins[name]?.enabled ?? true), version };
604
604
  });
605
- recordCheckedGeneration(scope, name, options.etag, { swapped: true });
605
+ await recordCheckedGeneration(scope, name, options.etag, { swapped: true });
606
606
  return { manifest, validated, version, root, staged };
607
607
  }
@@ -1,6 +1,6 @@
1
1
  import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig, ProviderOptionsConfig } from '../types.js';
2
2
  import { type BindingId } from './keybindings/catalog.js';
3
- import type { RotationConfig } from './subscription-state.js';
3
+ import type { RotationConfig } from './managed-providers.js';
4
4
  export declare function configPath(scope: Scope): string | null;
5
5
  export declare function statePath(scope: Scope): string | null;
6
6
  export interface BrokerConfigSnapshot {
@@ -26,16 +26,17 @@ export declare function readRawSettingsForTarget(target: RawSettingsTarget): {
26
26
  settings: Record<string, unknown> | null;
27
27
  revision: string;
28
28
  };
29
- export declare function replaceRawSettingsForTarget(target: RawSettingsTarget, next: Record<string, unknown>, ifRevision: string): {
29
+ export declare function replaceRawSettingsForTarget(target: RawSettingsTarget, next: Record<string, unknown>, ifRevision: string): Promise<{
30
30
  revision: string;
31
31
  changed: boolean;
32
- };
32
+ }>;
33
33
  type RawConfig = Record<string, unknown>;
34
- export declare function updateRawConfigAtomically(scope: Scope, mutate: (latest: RawConfig) => RawConfig): RawConfig;
35
- export declare function writeConfig(scope: Scope, config: ScopeConfig): void;
36
- export declare function writeRawConfig(scope: Scope, config: Partial<ScopeConfig>): void;
37
- export declare function writeState(scope: Scope, state: ScopeState): void;
38
- export declare function ensureScopeInitialized(scope: Scope, root: string): void;
34
+ export declare function updateRawConfigAtomically(scope: Scope, mutate: (latest: RawConfig) => RawConfig): Promise<RawConfig>;
35
+ export declare function writeConfig(scope: Scope, config: ScopeConfig): Promise<void>;
36
+ export declare function writeRawConfig(scope: Scope, config: Partial<ScopeConfig>): Promise<void>;
37
+ export declare function writeState(scope: Scope, state: ScopeState): Promise<void>;
38
+ /** A project scope is a directory holding `config.json`; the user and profile scopes are the person's rows and need no directory setup. */
39
+ export declare function ensureScopeInitialized(scope: Scope, root: string): Promise<void>;
39
40
  /** Normalize the user-owned list of attach actions allowed to bridge through an
40
41
  * occupied tmux root binding. Unknown and non-attach ids have no effect. */
41
42
  export declare function normalizeTmuxPassthrough(raw: unknown): BindingId[];
@@ -207,16 +208,16 @@ export declare function persistModelLadders(root: string, patch: {
207
208
  defaultProvider?: ModelProvider;
208
209
  anthropic?: Partial<Record<ModelStrength, string>>;
209
210
  openai?: Partial<Record<ModelStrength, string>>;
210
- }): void;
211
+ }): Promise<void>;
211
212
  /** Persist one complete route at one exact config root. The route is the only
212
213
  * raw route entry touched; unrelated routes and local ladder defaults remain
213
214
  * absent. Pass `undefined` to remove this scope's entry, which resumes lower
214
215
  * scope inheritance rather than introducing a tombstone. */
215
- export declare function persistModelRoute(root: string, routeId: string, route: ModelRouteConfig | undefined): void;
216
+ export declare function persistModelRoute(root: string, routeId: string, route: ModelRouteConfig | undefined): Promise<void>;
216
217
  /** Mutate one exact config root. Setup uses this for profile and explicit
217
218
  * project targets that cannot be represented by the user/project Scope enum. */
218
- export declare function updateConfigAtRoot(root: string, mutate: (cfg: ScopeConfig) => void): ScopeConfig;
219
- export declare function updateConfig(scope: Scope, mutate: (cfg: ScopeConfig) => void): ScopeConfig;
219
+ export declare function updateConfigAtRoot(root: string, mutate: (cfg: ScopeConfig) => void): Promise<ScopeConfig>;
220
+ export declare function updateConfig(scope: Scope, mutate: (cfg: ScopeConfig) => void): Promise<ScopeConfig>;
220
221
  /** Persist one sparse kind-model field at an exact config root. The sparse
221
222
  * patch keeps every unrelated kind and launch knob inherited from lower
222
223
  * scopes instead of freezing a materialized registry snapshot. */
@@ -225,9 +226,9 @@ export declare function persistDefaultKindModel(opts: {
225
226
  kind: string;
226
227
  model: string;
227
228
  orchestrator?: boolean;
228
- }): {
229
+ }): Promise<{
229
230
  path: string;
230
231
  field: 'model' | 'orchestratorModel';
231
- };
232
- export declare function updateState(scope: Scope, mutate: (s: ScopeState) => void): ScopeState;
232
+ }>;
233
+ export declare function updateState(scope: Scope, mutate: (s: ScopeState) => void): Promise<ScopeState>;
233
234
  export {};
@@ -10,9 +10,8 @@ import { atomicWriteJson, readJsonIfExists, ensureDir } from './fs-utils.js';
10
10
  import { scopeRoot, requireScopeRoot, findProjectScopeRoots, userScopeRoot } from './scope.js';
11
11
  import { listInstalledPluginsInRoot } from './installed-plugins.js';
12
12
  import { profileDir, listProfiles } from './profiles/manifest.js';
13
- import { canonicalRoot, projectStore, userStore } from './scoped-state/paths.js';
14
- import { convertLegacyScopedStore, initializeFreshScopedStore } from './scoped-state/migrate.js';
15
- import { mutateRawSettings, readBookkeeping, readRawSettings, readRawSettingsWithRevision, replaceRawSettings, writeBookkeeping, writeRawSettings } from './scoped-state/settings.js';
13
+ import { canonicalRoot } from './scoped-state/paths.js';
14
+ import { readCachedSettings, readCachedState, revisionOf, SettingsRevisionConflictError, writeSettings } from './scoped-state/person-store.js';
16
15
  import { withExclusiveDirectoryLock } from './exclusive-lock.js';
17
16
  import { SCOPE_CONFIG_KEYS } from './user-settings.js';
18
17
  import { normalizeWorkingGerunds } from '../shared/working-activity.js';
@@ -39,23 +38,18 @@ function settingsOwnerForRoot(root) {
39
38
  const canonical = canonicalRoot(root);
40
39
  const userRoot = canonicalRoot(userScopeRoot());
41
40
  if (canonical === userRoot)
42
- return { store: userStore(userRoot), key: 'user', kind: 'user', root: userRoot };
43
- // Project roots can be classified without opening the user store. This keeps a
44
- // fresh project usable when an unrelated legacy user root still needs conversion.
45
- if (basename(canonical) === '.crouter') {
46
- convertLegacyScopedStore({ kind: 'project', root: canonical });
47
- return { store: projectStore(canonical), key: 'project', kind: 'project', root: canonical };
48
- }
41
+ return { key: 'user', kind: 'user', root: userRoot };
42
+ if (basename(canonical) === '.crouter')
43
+ return { key: 'project', kind: 'project', root: canonical };
49
44
  for (const profile of listProfiles()) {
50
45
  const profilePath = canonicalRoot(profileDir(profile.profileId));
51
46
  if (canonical === profilePath)
52
- return { store: userStore(userRoot), key: `profile:${profile.profileId}`, kind: 'profile', root: profilePath, profileId: profile.profileId };
47
+ return { key: `profile:${profile.profileId}`, kind: 'profile', root: profilePath, profileId: profile.profileId };
53
48
  }
54
49
  throw new Error(`settings root is not a recognized owner: ${root}`);
55
50
  }
56
51
  function readRawAtRoot(root) {
57
- // Project configuration is a file; reading it must not initialize SQL state
58
- // (or its lock directory) after the project disappears.
52
+ // Project configuration is a file; reading it must not touch the person's rows (or its lock directory) after the project disappears.
59
53
  if (basename(root) === '.crouter') {
60
54
  let canonical;
61
55
  try {
@@ -69,21 +63,33 @@ function readRawAtRoot(root) {
69
63
  if (canonical !== canonicalRoot(userScopeRoot()))
70
64
  return readJsonIfExists(configPathFor(root));
71
65
  }
72
- return readRawSettings(settingsOwnerForRoot(root));
66
+ return readCachedSettings(settingsOwnerForRoot(root).key).settings;
67
+ }
68
+ /** Replace a user or profile owner's settings through the one write algorithm: `compute` runs against the cached raw settings and
69
+ * runs again if the rows changed underneath it. */
70
+ async function mutatePersonSettings(key, mutate) {
71
+ let value;
72
+ await writeSettings((snapshot) => {
73
+ const current = snapshot.settings[key];
74
+ const result = mutate(structuredClone(current ?? {}));
75
+ value = result.value;
76
+ return [{ op: 'settings.replace', owner: key, raw: result.next, ifRevision: revisionOf(current) }];
77
+ });
78
+ return value;
73
79
  }
74
- function writeRawAtRoot(root, raw) {
80
+ async function writeRawAtRoot(root, raw) {
75
81
  const owner = settingsOwnerForRoot(root);
76
82
  if (owner.kind === 'project') {
77
83
  withConfigPathLock(configPathFor(owner.root), (path) => atomicWriteJson(path, raw, { createParent: false }));
78
84
  return;
79
85
  }
80
- writeRawSettings(owner, raw);
86
+ await mutatePersonSettings(owner.key, () => ({ value: undefined, next: raw }));
81
87
  }
82
- function mutateRawAtRoot(root, mutate) {
88
+ async function mutateRawAtRoot(root, mutate) {
83
89
  const owner = settingsOwnerForRoot(root);
84
90
  if (owner.kind === 'project')
85
91
  return withConfigPathLock(configPathFor(owner.root), (path) => { const result = mutate(readJsonIfExists(path) ?? {}); atomicWriteJson(path, result.next, { createParent: false }); return result.value; });
86
- return mutateRawSettings(owner, (raw) => mutate(raw));
92
+ return await mutatePersonSettings(owner.key, mutate);
87
93
  }
88
94
  const brokerSnapshotState = globalThis;
89
95
  const brokerSnapshotKey = Symbol.for('crtr.broker.config-snapshot');
@@ -104,18 +110,17 @@ export function readConfig(scope) {
104
110
  return root ? readConfigAtRoot(root) : defaultScopeConfig();
105
111
  }
106
112
  export function readConfigAtRoot(root) { const existing = readRawAtRoot(root); return existing ? mergeConfig(existing) : defaultScopeConfig(); }
113
+ function readProjectState(root) {
114
+ const raw = readJsonIfExists(statePathFor(root));
115
+ return { marketplaces: raw?.marketplaces ?? {}, plugins: raw?.plugins ?? {}, activeCanvas: raw?.activeCanvas ?? null, ...(raw?.last_self_check === undefined ? {} : { last_self_check: raw.last_self_check }) };
116
+ }
107
117
  export function readState(scope) {
108
118
  const root = scopeRoot(scope);
109
119
  if (!root)
110
120
  return defaultScopeState();
111
- try {
112
- return readBookkeeping(settingsOwnerForRoot(root));
113
- }
114
- catch (error) {
115
- if (scope === 'project' && !existsSync(root))
116
- return defaultScopeState();
117
- throw error;
118
- }
121
+ if (scope === 'project')
122
+ return existsSync(root) ? readProjectState(root) : defaultScopeState();
123
+ return readCachedState(settingsOwnerForRoot(root).key);
119
124
  }
120
125
  export class InvalidRawSettingsError extends Error {
121
126
  constructor(cause) {
@@ -123,53 +128,64 @@ export class InvalidRawSettingsError extends Error {
123
128
  this.name = 'InvalidRawSettingsError';
124
129
  }
125
130
  }
126
- function rawSettingsOwnerForTarget(target) {
131
+ function rawSettingsKeyForTarget(target) {
127
132
  if (target.scope === 'user')
128
- return settingsOwnerForRoot(userScopeRoot());
133
+ return 'user';
129
134
  if (!listProfiles().some((profile) => profile.profileId === target.profileId)) {
130
135
  throw notFound(`profile not found: ${target.profileId}`, { received: target.profileId, next: 'Run `crtr profile list` to see available profiles.' });
131
136
  }
132
- return settingsOwnerForRoot(profileDir(target.profileId));
137
+ return `profile:${target.profileId}`;
133
138
  }
134
139
  export function readRawSettingsForTarget(target) {
135
- return readRawSettingsWithRevision(rawSettingsOwnerForTarget(target));
136
- }
137
- export function replaceRawSettingsForTarget(target, next, ifRevision) {
138
- return replaceRawSettings(rawSettingsOwnerForTarget(target), next, ifRevision, (settings) => {
139
- try {
140
- mergeConfig(settings);
141
- }
142
- catch (error) {
143
- throw new InvalidRawSettingsError(error);
144
- }
145
- });
140
+ return readCachedSettings(rawSettingsKeyForTarget(target));
146
141
  }
147
- export function updateRawConfigAtomically(scope, mutate) { const root = requireScopeRoot(scope); return mutateRawAtRoot(root, (latest) => { const next = mutate(latest); return { value: next, next }; }); }
148
- export function writeConfig(scope, config) { writeRawAtRoot(requireScopeRoot(scope), config); }
149
- export function writeRawConfig(scope, config) { writeRawAtRoot(requireScopeRoot(scope), config); }
150
- export function writeState(scope, state) { const root = requireScopeRoot(scope); writeBookkeeping(settingsOwnerForRoot(root), state); }
151
- export function ensureScopeInitialized(scope, root) {
142
+ export async function replaceRawSettingsForTarget(target, next, ifRevision) {
143
+ const key = rawSettingsKeyForTarget(target);
144
+ try {
145
+ mergeConfig(next);
146
+ }
147
+ catch (error) {
148
+ throw new InvalidRawSettingsError(error);
149
+ }
150
+ const before = readCachedSettings(key);
151
+ if (before.revision !== ifRevision)
152
+ throw new SettingsRevisionConflictError();
153
+ const after = await writeSettings((snapshot) => (revisionOf(snapshot.settings[key]) === ifRevision
154
+ ? [{ op: 'settings.replace', owner: key, raw: next, ifRevision }] : (() => { throw new SettingsRevisionConflictError(); })()));
155
+ const revision = revisionOf(after.settings[key]);
156
+ return { revision, changed: revision !== ifRevision };
157
+ }
158
+ export async function updateRawConfigAtomically(scope, mutate) { const root = requireScopeRoot(scope); return await mutateRawAtRoot(root, (latest) => { const next = mutate(latest); return { value: next, next }; }); }
159
+ export async function writeConfig(scope, config) { await writeRawAtRoot(requireScopeRoot(scope), config); }
160
+ export async function writeRawConfig(scope, config) { await writeRawAtRoot(requireScopeRoot(scope), config); }
161
+ function writeProjectState(root, state) { withConfigPathLock(statePathFor(root), (path) => atomicWriteJson(path, state, { createParent: false })); }
162
+ export async function writeState(scope, state) {
163
+ const root = requireScopeRoot(scope);
152
164
  if (scope === 'project') {
153
- try {
154
- mkdirSync(root);
155
- }
156
- catch (error) {
157
- if (error.code !== 'EEXIST' || !statSync(root).isDirectory())
158
- throw error;
159
- }
165
+ writeProjectState(root, state);
166
+ return;
160
167
  }
161
- else
168
+ const key = settingsOwnerForRoot(root).key;
169
+ await writeSettings(() => [{ op: 'state.write', owner: key, state }]);
170
+ }
171
+ /** A project scope is a directory holding `config.json`; the user and profile scopes are the person's rows and need no directory setup. */
172
+ export async function ensureScopeInitialized(scope, root) {
173
+ if (scope !== 'project') {
162
174
  ensureDir(root);
163
- const owner = scope === 'user' ? settingsOwnerForRoot(userScopeRoot()) : settingsOwnerForRoot(root);
164
- initializeFreshScopedStore({ kind: owner.kind === 'user' ? 'user' : 'project', root: owner.store.root });
165
- if (owner.kind === 'project' && readJsonIfExists(configPathFor(owner.root)) === null) {
166
- // A project config holds only what the project overrides. Seeding it with
167
- // the full default document would make every default (kinds, display
168
- // toggles, auto_update) a project-level override that silently shadows the
169
- // user's own settings for every directory beneath it — reads default-fill
170
- // missing keys from the running build, so a sparse file loses nothing.
171
- writeRawAtRoot(owner.root, { schema_version: defaultScopeConfig().schema_version });
175
+ return;
176
+ }
177
+ try {
178
+ mkdirSync(root);
172
179
  }
180
+ catch (error) {
181
+ if (error.code !== 'EEXIST' || !statSync(root).isDirectory())
182
+ throw error;
183
+ }
184
+ // A project config holds only what the project overrides. Seeding it with the full default document would make every default
185
+ // (kinds, display toggles, auto_update) a project-level override that silently shadows the user's own settings for every
186
+ // directory beneath it — reads default-fill missing keys from the running build, so a sparse file loses nothing.
187
+ if (readJsonIfExists(configPathFor(root)) === null)
188
+ await writeRawAtRoot(root, { schema_version: defaultScopeConfig().schema_version });
173
189
  }
174
190
  function normalizeKeybindings(raw) {
175
191
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
@@ -854,6 +870,13 @@ function mergeConfig(partial) {
854
870
  if (ledger?.take_pct !== undefined && (typeof ledger.take_pct !== 'number' || !Number.isFinite(ledger.take_pct) || ledger.take_pct < 0 || ledger.take_pct > 100)) {
855
871
  throw new Error('ledger.take_pct must be a number from 0 to 100');
856
872
  }
873
+ for (const key of ['sandbox_usd_per_gb_hour', 'sandbox_standby_s', 'storage_usd_per_gb_month', 'sandbox_disk_usd_per_gb_month', 'infra_free_usd_per_month']) {
874
+ const value = ledger?.[key];
875
+ if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value) || value < 0))
876
+ throw new Error(`ledger.${key} must be a number of at least 0`);
877
+ }
878
+ if (ledger?.r2_move !== undefined && typeof ledger.r2_move !== 'boolean')
879
+ throw new Error('ledger.r2_move must be true or false');
857
880
  const store = partial.store;
858
881
  const runtime = partial.runtime;
859
882
  if (runtime?.person_uids !== undefined && (!Array.isArray(runtime.person_uids)
@@ -1052,8 +1075,8 @@ function sparseModelLaddersToPersist(rawLadders, baseline, mutated) {
1052
1075
  * `sys config set` never freezes a full concrete `modelLadders` to disk,
1053
1076
  * and a direct ladder-rung set still persists only the touched rung -- all
1054
1077
  * through one lock-protected atomic update path. */
1055
- function mutateConfigAtRoot(root, mutate) {
1056
- return mutateRawAtRoot(root, (raw) => {
1078
+ async function mutateConfigAtRoot(root, mutate) {
1079
+ return await mutateRawAtRoot(root, (raw) => {
1057
1080
  const baseline = mergeConfig(raw);
1058
1081
  const cfg = mergeConfig(raw);
1059
1082
  mutate(cfg);
@@ -1080,8 +1103,8 @@ function mutateConfigAtRoot(root, mutate) {
1080
1103
  * default produces an empty diff and silently persists nothing, so the choice
1081
1104
  * evaporates as soon as the default moves. Explicit intent is recorded as
1082
1105
  * written. Mirrors `persistDefaultKindModel`. */
1083
- export function persistModelLadders(root, patch) {
1084
- mutateRawAtRoot(root, (raw) => { const current = (raw.modelLadders ?? {}); const next = { ...current }; if (patch.defaultProvider !== undefined)
1106
+ export async function persistModelLadders(root, patch) {
1107
+ await mutateRawAtRoot(root, (raw) => { const current = (raw.modelLadders ?? {}); const next = { ...current }; if (patch.defaultProvider !== undefined)
1085
1108
  next.defaultProvider = patch.defaultProvider; for (const provider of ['anthropic', 'openai'])
1086
1109
  if (patch[provider] !== undefined)
1087
1110
  next[provider] = { ...(current[provider] ?? {}), ...patch[provider] }; return { value: undefined, next: { ...raw, modelLadders: next } }; });
@@ -1090,12 +1113,12 @@ export function persistModelLadders(root, patch) {
1090
1113
  * raw route entry touched; unrelated routes and local ladder defaults remain
1091
1114
  * absent. Pass `undefined` to remove this scope's entry, which resumes lower
1092
1115
  * scope inheritance rather than introducing a tombstone. */
1093
- export function persistModelRoute(root, routeId, route) {
1116
+ export async function persistModelRoute(root, routeId, route) {
1094
1117
  if (route !== undefined)
1095
1118
  assertCompleteModelRoute(routeId, route);
1096
1119
  else if (routeId.trim() === '')
1097
1120
  throw new Error('model route id must be a non-empty string');
1098
- mutateRawAtRoot(root, (raw) => { const routes = copyRouteRecord((raw.modelRoutes ?? {})); if (route === undefined)
1121
+ await mutateRawAtRoot(root, (raw) => { const routes = copyRouteRecord((raw.modelRoutes ?? {})); if (route === undefined)
1099
1122
  delete routes[routeId];
1100
1123
  else
1101
1124
  setRouteRecord(routes, routeId, { ...route, models: { ...route.models } }); const next = { ...raw }; if (Object.keys(routes).length === 0)
@@ -1105,23 +1128,23 @@ export function persistModelRoute(root, routeId, route) {
1105
1128
  }
1106
1129
  /** Mutate one exact config root. Setup uses this for profile and explicit
1107
1130
  * project targets that cannot be represented by the user/project Scope enum. */
1108
- export function updateConfigAtRoot(root, mutate) {
1109
- return mutateConfigAtRoot(root, mutate);
1131
+ export async function updateConfigAtRoot(root, mutate) {
1132
+ return await mutateConfigAtRoot(root, mutate);
1110
1133
  }
1111
- export function updateConfig(scope, mutate) {
1112
- return updateConfigAtRoot(requireScopeRoot(scope), mutate);
1134
+ export async function updateConfig(scope, mutate) {
1135
+ return await updateConfigAtRoot(requireScopeRoot(scope), mutate);
1113
1136
  }
1114
1137
  /** Persist one sparse kind-model field at an exact config root. The sparse
1115
1138
  * patch keeps every unrelated kind and launch knob inherited from lower
1116
1139
  * scopes instead of freezing a materialized registry snapshot. */
1117
- export function persistDefaultKindModel(opts) {
1140
+ export async function persistDefaultKindModel(opts) {
1118
1141
  const field = opts.orchestrator === true ? 'orchestratorModel' : 'model';
1119
- mutateRawAtRoot(opts.root, (raw) => { const kinds = { ...(raw.kinds ?? {}), [opts.kind]: { ...(raw.kinds ?? {})[opts.kind], [field]: opts.model } }; return { value: undefined, next: { ...raw, kinds: kinds } }; });
1142
+ await mutateRawAtRoot(opts.root, (raw) => { const kinds = { ...(raw.kinds ?? {}), [opts.kind]: { ...(raw.kinds ?? {})[opts.kind], [field]: opts.model } }; return { value: undefined, next: { ...raw, kinds: kinds } }; });
1120
1143
  return { path: configPathFor(opts.root), field };
1121
1144
  }
1122
- export function updateState(scope, mutate) {
1145
+ export async function updateState(scope, mutate) {
1123
1146
  const s = readState(scope);
1124
1147
  mutate(s);
1125
- writeState(scope, s);
1148
+ await writeState(scope, s);
1126
1149
  return s;
1127
1150
  }
@@ -0,0 +1,2 @@
1
+ import type { ConversationStoreClient } from './index.js';
2
+ export declare function createStoreClient(address: string, token?: string): ConversationStoreClient;