@fungi.computer/shiitake 0.0.0-stage → 0.0.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 (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -2
  3. package/dist/agar-extension.d.ts +9 -0
  4. package/dist/agar-extension.js +38 -0
  5. package/dist/agent-control.d.ts +72 -0
  6. package/dist/agent-control.js +431 -0
  7. package/dist/client-entry.d.ts +211 -0
  8. package/dist/client-entry.js +101 -0
  9. package/dist/client-wire.d.ts +199 -0
  10. package/dist/client-wire.js +271 -0
  11. package/dist/client.d.ts +439 -0
  12. package/dist/client.js +4 -0
  13. package/dist/cloudflare.d.ts +220 -0
  14. package/dist/cloudflare.js +531 -0
  15. package/dist/effect.d.ts +42 -0
  16. package/dist/effect.js +20 -0
  17. package/dist/errors.d.ts +50 -0
  18. package/dist/errors.js +81 -0
  19. package/dist/extension-client.d.ts +38 -0
  20. package/dist/extension-client.js +56 -0
  21. package/dist/extension-contract.d.ts +73 -0
  22. package/dist/extension-contract.js +11 -0
  23. package/dist/extensions.d.ts +589 -0
  24. package/dist/extensions.js +263 -0
  25. package/dist/fleet.d.ts +297 -0
  26. package/dist/fleet.js +515 -0
  27. package/dist/hosted.d.ts +133 -0
  28. package/dist/hosted.js +265 -0
  29. package/dist/http-routes.d.ts +772 -0
  30. package/dist/http-routes.js +242 -0
  31. package/dist/http.d.ts +1 -0
  32. package/dist/http.js +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.js +13 -0
  35. package/dist/internal/accepted-run-executor.d.ts +23 -0
  36. package/dist/internal/accepted-run-executor.js +129 -0
  37. package/dist/internal/admission-door.d.ts +75 -0
  38. package/dist/internal/admission-door.js +105 -0
  39. package/dist/internal/agent-close.d.ts +4 -0
  40. package/dist/internal/agent-close.js +42 -0
  41. package/dist/internal/browser-sessions.d.ts +31 -0
  42. package/dist/internal/browser-sessions.js +173 -0
  43. package/dist/internal/client-boundary.d.ts +262 -0
  44. package/dist/internal/client-boundary.js +202 -0
  45. package/dist/internal/compaction/compaction.d.ts +54 -0
  46. package/dist/internal/compaction/compaction.js +292 -0
  47. package/dist/internal/compaction/session-compaction.d.ts +69 -0
  48. package/dist/internal/compaction/session-compaction.js +330 -0
  49. package/dist/internal/compaction/settings.d.ts +11 -0
  50. package/dist/internal/compaction/settings.js +40 -0
  51. package/dist/internal/compaction/utils.d.ts +45 -0
  52. package/dist/internal/compaction/utils.js +319 -0
  53. package/dist/internal/contracts.d.ts +235 -0
  54. package/dist/internal/contracts.js +1 -0
  55. package/dist/internal/coordinator.d.ts +18 -0
  56. package/dist/internal/coordinator.js +362 -0
  57. package/dist/internal/document-codec.d.ts +11 -0
  58. package/dist/internal/document-codec.js +39 -0
  59. package/dist/internal/effect-client.d.ts +13 -0
  60. package/dist/internal/effect-client.js +102 -0
  61. package/dist/internal/effect-contracts.d.ts +236 -0
  62. package/dist/internal/effect-contracts.js +2 -0
  63. package/dist/internal/extension-command-registry.d.ts +11 -0
  64. package/dist/internal/extension-command-registry.js +29 -0
  65. package/dist/internal/extension-command-tools.d.ts +17 -0
  66. package/dist/internal/extension-command-tools.js +50 -0
  67. package/dist/internal/extension-context.d.ts +25 -0
  68. package/dist/internal/extension-context.js +35 -0
  69. package/dist/internal/extension-definitions.d.ts +5 -0
  70. package/dist/internal/extension-definitions.js +6 -0
  71. package/dist/internal/extension-hook.d.ts +25 -0
  72. package/dist/internal/extension-hook.js +46 -0
  73. package/dist/internal/extension-roster.d.ts +23 -0
  74. package/dist/internal/extension-roster.js +228 -0
  75. package/dist/internal/extension-runtime.d.ts +63 -0
  76. package/dist/internal/extension-runtime.js +1004 -0
  77. package/dist/internal/failure.d.ts +2 -0
  78. package/dist/internal/failure.js +9 -0
  79. package/dist/internal/host-meta.d.ts +41 -0
  80. package/dist/internal/host-meta.js +39 -0
  81. package/dist/internal/json-value.d.ts +4 -0
  82. package/dist/internal/json-value.js +3 -0
  83. package/dist/internal/model-acquisition.d.ts +21 -0
  84. package/dist/internal/model-acquisition.js +98 -0
  85. package/dist/internal/model-preference.d.ts +21 -0
  86. package/dist/internal/model-preference.js +20 -0
  87. package/dist/internal/observation-kernel.d.ts +28 -0
  88. package/dist/internal/observation-kernel.js +169 -0
  89. package/dist/internal/promise-facade.d.ts +18 -0
  90. package/dist/internal/promise-facade.js +172 -0
  91. package/dist/internal/run-scope.d.ts +27 -0
  92. package/dist/internal/run-scope.js +214 -0
  93. package/dist/internal/runtime-coordinator.d.ts +34 -0
  94. package/dist/internal/runtime-coordinator.js +233 -0
  95. package/dist/internal/runtime-core.d.ts +19 -0
  96. package/dist/internal/runtime-core.js +39 -0
  97. package/dist/internal/runtime-reconciliation.d.ts +18 -0
  98. package/dist/internal/runtime-reconciliation.js +162 -0
  99. package/dist/internal/session-context.d.ts +11 -0
  100. package/dist/internal/session-context.js +67 -0
  101. package/dist/internal/session-status.d.ts +10 -0
  102. package/dist/internal/session-status.js +52 -0
  103. package/dist/internal/woodstock/agent-reply.d.ts +25 -0
  104. package/dist/internal/woodstock/agent-reply.js +67 -0
  105. package/dist/internal/woodstock/client-projection.d.ts +29 -0
  106. package/dist/internal/woodstock/client-projection.js +512 -0
  107. package/dist/internal/woodstock/compaction-activity.d.ts +17 -0
  108. package/dist/internal/woodstock/compaction-activity.js +32 -0
  109. package/dist/internal/woodstock/cursor.d.ts +9 -0
  110. package/dist/internal/woodstock/cursor.js +44 -0
  111. package/dist/internal/woodstock/extension-store.d.ts +72 -0
  112. package/dist/internal/woodstock/extension-store.js +262 -0
  113. package/dist/internal/woodstock/fleet-store.d.ts +32 -0
  114. package/dist/internal/woodstock/fleet-store.js +93 -0
  115. package/dist/internal/woodstock/identities.d.ts +6 -0
  116. package/dist/internal/woodstock/identities.js +11 -0
  117. package/dist/internal/woodstock/index.d.ts +170 -0
  118. package/dist/internal/woodstock/index.js +546 -0
  119. package/dist/internal/woodstock/progress.d.ts +32 -0
  120. package/dist/internal/woodstock/progress.js +89 -0
  121. package/dist/internal/woodstock/run-recovery.d.ts +8 -0
  122. package/dist/internal/woodstock/run-recovery.js +95 -0
  123. package/dist/internal/woodstock/session-attention.d.ts +16 -0
  124. package/dist/internal/woodstock/session-attention.js +88 -0
  125. package/dist/internal/woodstock/session-documents.d.ts +18 -0
  126. package/dist/internal/woodstock/session-documents.js +51 -0
  127. package/dist/internal/woodstock/session-entry.d.ts +478 -0
  128. package/dist/internal/woodstock/session-entry.js +484 -0
  129. package/dist/internal/woodstock/session-grants.d.ts +24 -0
  130. package/dist/internal/woodstock/session-grants.js +84 -0
  131. package/dist/internal/woodstock/session-inbox.d.ts +56 -0
  132. package/dist/internal/woodstock/session-inbox.js +307 -0
  133. package/dist/internal/woodstock/session-store.d.ts +432 -0
  134. package/dist/internal/woodstock/session-store.js +2006 -0
  135. package/dist/internal/woodstock/storage-admission.d.ts +14 -0
  136. package/dist/internal/woodstock/storage-admission.js +44 -0
  137. package/dist/model-preference.d.ts +20 -0
  138. package/dist/model-preference.js +30 -0
  139. package/dist/node.d.ts +64 -0
  140. package/dist/node.js +256 -0
  141. package/dist/remote-client.d.ts +11 -0
  142. package/dist/remote-client.js +298 -0
  143. package/dist/session-attention.d.ts +35 -0
  144. package/dist/session-attention.js +22 -0
  145. package/dist/session-documents.d.ts +28 -0
  146. package/dist/session-documents.js +1 -0
  147. package/dist/testing.d.ts +54 -0
  148. package/dist/testing.js +215 -0
  149. package/docs/api/README.md +26 -0
  150. package/docs/api/agar-extension.md +40 -0
  151. package/docs/api/agent-control.md +151 -0
  152. package/docs/api/client-entry.md +344 -0
  153. package/docs/api/client.md +1112 -0
  154. package/docs/api/cloudflare.md +500 -0
  155. package/docs/api/effect.md +904 -0
  156. package/docs/api/extensions.md +2003 -0
  157. package/docs/api/fleet.md +706 -0
  158. package/docs/api/hosted.md +382 -0
  159. package/docs/api/http.md +111 -0
  160. package/docs/api/index.md +1116 -0
  161. package/docs/api/model-preference.md +151 -0
  162. package/docs/api/node.md +142 -0
  163. package/docs/api/testing.md +250 -0
  164. package/package.json +130 -3
@@ -0,0 +1,14 @@
1
+ import type { StorageBudget } from "../contracts.js";
2
+ /** The admission check for writes that start new work. */
3
+ export type StorageAdmission = Readonly<{
4
+ /**
5
+ * Throw a typed `storage_pressure` refusal when the owner is near its
6
+ * budget. Called only for a new Session or a new prompt, never for a
7
+ * replayed request or for writes that finish admitted work.
8
+ */
9
+ admitNew: () => void;
10
+ }>;
11
+ /** The admission of a host that set no budget: nothing is refused. */
12
+ export declare const admitWithoutBudget: StorageAdmission;
13
+ /** Open the owner's admission check over the host's measured budget. */
14
+ export declare function openStorageAdmission(budget: StorageBudget | undefined): StorageAdmission;
@@ -0,0 +1,44 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { ShiitakeError } from "../../errors.js";
3
+ /**
4
+ * Provisional disk thresholds, as fractions of the host's budget. At
5
+ * `refuseAt` new Sessions and new prompts are refused; once refusing, they
6
+ * are admitted again only below `readmitBelow`, so admission does not flap
7
+ * while usage hovers at the threshold. The gap is the room the work already
8
+ * admitted keeps for finishing: its writes never pass through this check.
9
+ */
10
+ const storagePressureThresholds = Object.freeze({
11
+ refuseAt: 0.8,
12
+ readmitBelow: 0.75,
13
+ });
14
+ const decodeBudgetBytes = Schema.decodeUnknownResult(Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0))));
15
+ const decodeUsedBytes = Schema.decodeUnknownResult(Schema.Number.pipe(Schema.check(Schema.isFinite()), Schema.check(Schema.isGreaterThanOrEqualTo(0))));
16
+ /** The admission of a host that set no budget: nothing is refused. */
17
+ export const admitWithoutBudget = Object.freeze({
18
+ admitNew: () => {
19
+ // No budget, so there is no storage pressure to refuse for.
20
+ },
21
+ });
22
+ /** Open the owner's admission check over the host's measured budget. */
23
+ export function openStorageAdmission(budget) {
24
+ if (budget === undefined)
25
+ return admitWithoutBudget;
26
+ const budgetBytes = decodeBudgetBytes(budget.budgetBytes);
27
+ if (budgetBytes._tag === "Failure")
28
+ throw new ShiitakeError("contract", "invalid", "Storage budget must be a positive whole number of bytes");
29
+ const limit = budgetBytes.success;
30
+ let refusing = false;
31
+ return Object.freeze({
32
+ admitNew: () => {
33
+ const used = decodeUsedBytes(budget.usedBytes());
34
+ if (used._tag === "Failure")
35
+ throw new ShiitakeError("store", "unavailable", "Owner storage size is unavailable");
36
+ const fraction = used.success / limit;
37
+ refusing = refusing
38
+ ? fraction >= storagePressureThresholds.readmitBelow
39
+ : fraction >= storagePressureThresholds.refuseAt;
40
+ if (refusing)
41
+ throw new ShiitakeError("store", "storage_pressure", "Storage is near its budget. New Sessions and prompts are refused until space is freed; running work continues.");
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,20 @@
1
+ import * as JsonSchema from "effect/JsonSchema";
2
+ import { ModelPreferenceSchema, parseModelPreference, parseModelPreferenceJson } from "./internal/model-preference.js";
3
+ import type { ModelPreference } from "./internal/model-preference.js";
4
+ export { ModelPreferenceSchema, parseModelPreference, parseModelPreferenceJson, };
5
+ export type { ModelPreference } from "./internal/model-preference.js";
6
+ /** JSON Schema generated from Shiitake's maintained model preference schema. */
7
+ export declare const modelPreferenceJsonSchema: JsonSchema.JsonSchema;
8
+ /** Parse the footer's provider/model spelling; model ids may contain slashes. */
9
+ export declare function parseModelAddress(input: unknown): ModelPreference;
10
+ /**
11
+ * Resolve host-owned defaults for a future run. Hosts supply offered, logged-in
12
+ * availability; an explicit choice never silently falls through on auth failure.
13
+ */
14
+ export declare function resolveModelPreference(layers: Readonly<{
15
+ session?: ModelPreference | null | undefined;
16
+ agent?: ModelPreference | null | undefined;
17
+ manual?: ModelPreference | null | undefined;
18
+ team?: ModelPreference | null | undefined;
19
+ available?: ModelPreference | null | undefined;
20
+ }>): ModelPreference | null;
@@ -0,0 +1,30 @@
1
+ import * as JsonSchema from "effect/JsonSchema";
2
+ import * as Schema from "effect/Schema";
3
+ import { ModelPreferenceSchema, parseModelPreference, parseModelPreferenceJson, } from "./internal/model-preference.js";
4
+ export { ModelPreferenceSchema, parseModelPreference, parseModelPreferenceJson, };
5
+ /** JSON Schema generated from Shiitake's maintained model preference schema. */
6
+ export const modelPreferenceJsonSchema = JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(ModelPreferenceSchema, {
7
+ onExcessProperty: "error",
8
+ })).schema;
9
+ const modelAddressSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^[^/\s]+\/[^\s]+$/u)), Schema.check(Schema.isMaxLength(513)));
10
+ /** Parse the footer's provider/model spelling; model ids may contain slashes. */
11
+ export function parseModelAddress(input) {
12
+ const address = Schema.decodeUnknownSync(modelAddressSchema)(input);
13
+ const slash = address.indexOf("/");
14
+ return Schema.decodeUnknownSync(ModelPreferenceSchema)({
15
+ providerId: address.slice(0, slash),
16
+ modelId: address.slice(slash + 1),
17
+ });
18
+ }
19
+ /**
20
+ * Resolve host-owned defaults for a future run. Hosts supply offered, logged-in
21
+ * availability; an explicit choice never silently falls through on auth failure.
22
+ */
23
+ export function resolveModelPreference(layers) {
24
+ return (layers.session ??
25
+ layers.agent ??
26
+ layers.manual ??
27
+ layers.team ??
28
+ layers.available ??
29
+ null);
30
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { Hono } from "hono";
2
+ import { type ShiitakeHttpEnv } from "./http-routes.js";
3
+ import type { RuntimeHost, ShiitakeOptions, ShiitakeOwnerAgent } from "./internal/contracts.js";
4
+ /** The event-loop runtime host one Node process gives one durable Agent. */
5
+ export type NodeRuntimeHost = Readonly<{
6
+ /** Pass as `runtime` to `Shiitake.make`. */
7
+ runtime: RuntimeHost;
8
+ /** Ask for a reconciliation at an epoch-millisecond time; null clears it. */
9
+ scheduleWake: (at: number | null) => void;
10
+ /** Stop waking and await the reconciliation in flight. */
11
+ close: () => Promise<void>;
12
+ }>;
13
+ /**
14
+ * The Node event loop as Shiitake's runtime host: one reconciliation at a
15
+ * time, re-run when woken during a pass, and a timer for the earliest due
16
+ * retry. It has no queue policy; Watchdog owns execution and retries.
17
+ * `observeFailure` sees a failed pass, which is retried after five seconds;
18
+ * if it throws, the throw is logged and the retry still happens.
19
+ */
20
+ export declare function nodeRuntimeHost(observeFailure: (cause: unknown) => void): NodeRuntimeHost;
21
+ /** An exclusive, process-held claim on one store file. */
22
+ export type NodeStoreLease = Readonly<{
23
+ /** End the claim; later calls do nothing. */
24
+ release: () => void;
25
+ }>;
26
+ /**
27
+ * Claim one store for this process: an exclusive transaction held open on a
28
+ * sidecar file, so a second owner of the same store is refused at once and
29
+ * the claim ends with the process, never with a missed heartbeat.
30
+ */
31
+ export declare function acquireNodeStoreLease(sidecarPath: string): NodeStoreLease;
32
+ /** Everything a Node host supplies except the store and runtime it owns. */
33
+ export type NodeShiitakeOptions = Omit<ShiitakeOptions, "store" | "runtime"> & Readonly<{
34
+ /**
35
+ * The SQLite file this Agent lives in; created when missing, private to
36
+ * this user. It may not be a symbolic link or have another hard link.
37
+ */
38
+ path: string;
39
+ /**
40
+ * Observe a failed reconciliation pass; it is retried after 5 seconds.
41
+ * Absent, the failure goes to Effect's logger.
42
+ */
43
+ onRuntimeFailure?: (cause: unknown) => void;
44
+ }>;
45
+ /** One durable Agent in a SQLite file, owned by this Node process. */
46
+ export type NodeShiitake = Readonly<{
47
+ /** The in-process owner: sends, metadata views and grant deliveries. */
48
+ agent: ShiitakeOwnerAgent;
49
+ /**
50
+ * Shiitake's Session routes as a Hono app. Serve it with any Node or Bun
51
+ * server, behind host middleware that alone sets `shiitakeMeta`.
52
+ */
53
+ routes: Hono<ShiitakeHttpEnv>;
54
+ /** Stop waking, close the Agent, then release the store. */
55
+ close: () => Promise<void>;
56
+ }>;
57
+ /**
58
+ * Open one durable Agent in a SQLite file on disk. The process holds an
59
+ * exclusive lease on the store until `close`, so a second opener is refused.
60
+ * The store and its lease file are private to this user (0600), and a store
61
+ * reached through a symbolic link or with a second hard link is refused.
62
+ * Accepted work survives a restart: reopening the file resumes it.
63
+ */
64
+ export declare function openNodeShiitake(options: NodeShiitakeOptions): Promise<NodeShiitake>;
package/dist/node.js ADDED
@@ -0,0 +1,256 @@
1
+ import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync, realpathSync, } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import * as Effect from "effect/Effect";
5
+ import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
6
+ import { createShiitakeHttpRoutes, } from "./http-routes.js";
7
+ import { Shiitake } from "./index.js";
8
+ /** A retry hint after a failed or still-pending reconciliation pass. */
9
+ const retryAfterMs = 5_000;
10
+ /**
11
+ * The Node event loop as Shiitake's runtime host: one reconciliation at a
12
+ * time, re-run when woken during a pass, and a timer for the earliest due
13
+ * retry. It has no queue policy; Watchdog owns execution and retries.
14
+ * `observeFailure` sees a failed pass, which is retried after five seconds;
15
+ * if it throws, the throw is logged and the retry still happens.
16
+ */
17
+ export function nodeRuntimeHost(observeFailure) {
18
+ // A throwing observer must never cost the retry or crash the process.
19
+ const reportFailure = (cause) => {
20
+ try {
21
+ observeFailure(cause);
22
+ }
23
+ catch (observerCause) {
24
+ Effect.runFork(Effect.logError("shiitake_failure_observer_threw", {
25
+ cause: observerCause,
26
+ failure: String(cause),
27
+ }));
28
+ }
29
+ };
30
+ let bound;
31
+ let wakeTimer;
32
+ let wakeAt;
33
+ let closed = false;
34
+ const scheduleWake = (at) => {
35
+ if (closed)
36
+ return;
37
+ if (at === null) {
38
+ if (wakeTimer !== undefined)
39
+ clearTimeout(wakeTimer);
40
+ wakeTimer = undefined;
41
+ wakeAt = undefined;
42
+ return;
43
+ }
44
+ // Keep the earliest retry hint; later requests must not postpone it.
45
+ if (wakeTimer !== undefined && wakeAt !== undefined && at >= wakeAt)
46
+ return;
47
+ if (wakeTimer !== undefined)
48
+ clearTimeout(wakeTimer);
49
+ wakeAt = at;
50
+ wakeTimer = setTimeout(() => {
51
+ wakeTimer = undefined;
52
+ wakeAt = undefined;
53
+ void bound?.requestWake().catch(reportFailure);
54
+ }, Math.max(0, at - Date.now()));
55
+ };
56
+ const runtime = Object.freeze({
57
+ bind: ({ reconcile, pendingWork }) => {
58
+ if (bound !== undefined)
59
+ return Promise.reject(new Error("Shiitake Node runtime is already bound"));
60
+ let active;
61
+ let pending;
62
+ let rerun = false;
63
+ let closing;
64
+ const run = () => {
65
+ if (active !== undefined)
66
+ return;
67
+ const task = Promise.resolve().then(async () => {
68
+ try {
69
+ await reconcile();
70
+ const work = await pendingWork();
71
+ scheduleWake(work.pending ? Date.now() + retryAfterMs : work.nextWakeAt);
72
+ }
73
+ catch (cause) {
74
+ reportFailure(cause);
75
+ scheduleWake(Date.now() + retryAfterMs);
76
+ }
77
+ finally {
78
+ if (active === task)
79
+ active = undefined;
80
+ if (rerun) {
81
+ rerun = false;
82
+ // A wake admitted before close still drains its one rerun.
83
+ if (closed)
84
+ run();
85
+ else
86
+ schedule();
87
+ }
88
+ }
89
+ });
90
+ active = task;
91
+ };
92
+ const schedule = () => {
93
+ if (closed || active !== undefined || pending !== undefined)
94
+ return;
95
+ pending = setImmediate(() => {
96
+ pending = undefined;
97
+ if (!closed)
98
+ run();
99
+ });
100
+ };
101
+ const handle = Object.freeze({
102
+ commit: (mutation) => mutation(),
103
+ requestWake: () => {
104
+ if (closed)
105
+ return Promise.reject(new Error("Shiitake Node runtime is closed"));
106
+ if (active !== undefined)
107
+ rerun = true;
108
+ else
109
+ schedule();
110
+ return Promise.resolve();
111
+ },
112
+ close: () => {
113
+ closing ??= (async () => {
114
+ closed = true;
115
+ if (wakeTimer !== undefined)
116
+ clearTimeout(wakeTimer);
117
+ wakeTimer = undefined;
118
+ wakeAt = undefined;
119
+ if (pending !== undefined)
120
+ clearImmediate(pending);
121
+ pending = undefined;
122
+ while (active !== undefined)
123
+ await active;
124
+ })();
125
+ return closing;
126
+ },
127
+ });
128
+ schedule();
129
+ bound = handle;
130
+ return Promise.resolve(handle);
131
+ },
132
+ });
133
+ return Object.freeze({
134
+ runtime,
135
+ scheduleWake,
136
+ close: () => bound?.close() ?? Promise.resolve(),
137
+ });
138
+ }
139
+ /**
140
+ * Claim one store for this process: an exclusive transaction held open on a
141
+ * sidecar file, so a second owner of the same store is refused at once and
142
+ * the claim ends with the process, never with a missed heartbeat.
143
+ */
144
+ export function acquireNodeStoreLease(sidecarPath) {
145
+ const database = new DatabaseSync(sidecarPath, { timeout: 0 });
146
+ try {
147
+ database.exec("PRAGMA busy_timeout = 0");
148
+ database.exec("PRAGMA journal_mode = DELETE");
149
+ database.exec("BEGIN EXCLUSIVE");
150
+ }
151
+ catch (cause) {
152
+ database.close();
153
+ throw cause;
154
+ }
155
+ let released = false;
156
+ return Object.freeze({
157
+ release: () => {
158
+ if (released)
159
+ return;
160
+ released = true;
161
+ try {
162
+ database.exec("ROLLBACK");
163
+ }
164
+ finally {
165
+ database.close();
166
+ }
167
+ },
168
+ });
169
+ }
170
+ /**
171
+ * Create or open one store file for this process alone: never through a
172
+ * symbolic link, never a file with a second hard link (either would let a
173
+ * second owner claim the same bytes under another name), and readable by
174
+ * this user only, whatever the umask.
175
+ */
176
+ function claimPrivateFile(path) {
177
+ const descriptor = openSync(path, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
178
+ try {
179
+ const metadata = fstatSync(descriptor);
180
+ if (!metadata.isFile() || metadata.nlink !== 1)
181
+ throw new Error(`Shiitake Node store must be one regular file with one name: ${path}`);
182
+ fchmodSync(descriptor, 0o600);
183
+ }
184
+ finally {
185
+ closeSync(descriptor);
186
+ }
187
+ }
188
+ /**
189
+ * The one name every opener of a store agrees on: its real directory (created
190
+ * private when missing) and its own file name, so a linked directory still
191
+ * meets the same lease.
192
+ */
193
+ function canonicalStorePath(path) {
194
+ const directory = dirname(resolve(path));
195
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
196
+ return join(realpathSync(directory), basename(path));
197
+ }
198
+ /**
199
+ * Open one durable Agent in a SQLite file on disk. The process holds an
200
+ * exclusive lease on the store until `close`, so a second opener is refused.
201
+ * The store and its lease file are private to this user (0600), and a store
202
+ * reached through a symbolic link or with a second hard link is refused.
203
+ * Accepted work survives a restart: reopening the file resumes it.
204
+ */
205
+ export async function openNodeShiitake(options) {
206
+ const { path: requested, onRuntimeFailure, ...shiitakeOptions } = options;
207
+ const path = canonicalStorePath(requested);
208
+ const sidecarPath = `${path}.owner.sqlite`;
209
+ claimPrivateFile(sidecarPath);
210
+ const lease = acquireNodeStoreLease(sidecarPath);
211
+ let database;
212
+ let host;
213
+ try {
214
+ // Checked under the lease: a link made after this is refused by the next
215
+ // opener's own check, before it can open the bytes.
216
+ claimPrivateFile(path);
217
+ const opened = new DatabaseSync(path, { timeout: 0 });
218
+ database = opened;
219
+ opened.exec("PRAGMA foreign_keys = ON");
220
+ const runtime = nodeRuntimeHost(onRuntimeFailure ??
221
+ ((cause) => {
222
+ Effect.runFork(Effect.logError("shiitake_reconciliation_failed").pipe(Effect.annotateLogs({ cause: String(cause) })));
223
+ }));
224
+ host = runtime;
225
+ const agent = await Shiitake.make({
226
+ ...shiitakeOptions,
227
+ store: fromNodeSqlite(opened),
228
+ runtime: runtime.runtime,
229
+ });
230
+ const routes = createShiitakeHttpRoutes((sessionId, meta) => (meta === undefined ? agent : agent.withMeta(meta)).session(sessionId), agent.listSessions, agent.createSession, agent.deleteSession, () => agent.extensions);
231
+ let closing;
232
+ return Object.freeze({
233
+ agent,
234
+ routes,
235
+ close: () => {
236
+ closing ??= (async () => {
237
+ try {
238
+ await agent.close();
239
+ await runtime.close();
240
+ }
241
+ finally {
242
+ opened.close();
243
+ lease.release();
244
+ }
245
+ })();
246
+ return closing;
247
+ },
248
+ });
249
+ }
250
+ catch (cause) {
251
+ await host?.close();
252
+ database?.close();
253
+ lease.release();
254
+ throw cause;
255
+ }
256
+ }
@@ -0,0 +1,11 @@
1
+ import type { ShiitakeEventSourceConstructor, ShiitakeAgent } from "./client.js";
2
+ /** Network location of one remotely hosted Shiitake agent. */
3
+ export type ShiitakeClientOptions = Readonly<{
4
+ baseUrl: string;
5
+ /** Host-owned HTTP transport, including authenticated internal bindings. */
6
+ fetch?: typeof globalThis.fetch;
7
+ /** Optional runtime EventSource constructor; browsers use the global by default. */
8
+ eventSource?: ShiitakeEventSourceConstructor;
9
+ }>;
10
+ /** Connect to a remotely hosted Shiitake agent. */
11
+ export declare function createShiitakeClient(options: ShiitakeClientOptions): ShiitakeAgent;