@miadi/connector 0.1.19 → 0.1.21

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 (43) hide show
  1. package/README.md +40 -5
  2. package/dist/approve.d.ts +16 -2
  3. package/dist/approve.js +185 -16
  4. package/dist/bin.js +3 -0
  5. package/dist/mission-status.d.ts +13 -1
  6. package/dist/mission-status.js +13 -5
  7. package/dist/model-preflight.d.ts +9 -2
  8. package/dist/model-preflight.js +23 -1
  9. package/dist/provider-policy.d.ts +94 -0
  10. package/dist/provider-policy.js +150 -0
  11. package/dist/providers.d.ts +7 -3
  12. package/dist/providers.js +13 -7
  13. package/dist/queue-state.js +12 -1
  14. package/dist/reading/adapters.d.ts +18 -0
  15. package/dist/reading/adapters.js +90 -0
  16. package/dist/reading/contract.d.ts +153 -0
  17. package/dist/reading/contract.js +79 -0
  18. package/dist/reading/markdown.d.ts +47 -0
  19. package/dist/reading/markdown.js +52 -0
  20. package/dist/reading/privacy.d.ts +7 -0
  21. package/dist/reading/privacy.js +14 -0
  22. package/dist/reading/workflow.d.ts +93 -0
  23. package/dist/reading/workflow.js +325 -0
  24. package/dist/records.d.ts +1 -1
  25. package/dist/records.js +6 -0
  26. package/dist/request-state.js +9 -1
  27. package/dist/research-digest.d.ts +30 -0
  28. package/dist/research-digest.js +104 -0
  29. package/dist/research-shelf.d.ts +173 -0
  30. package/dist/research-shelf.js +297 -0
  31. package/dist/runtime-steer.d.ts +182 -0
  32. package/dist/runtime-steer.js +543 -0
  33. package/dist/server.d.ts +14 -0
  34. package/dist/server.js +4 -12
  35. package/dist/steer.d.ts +21 -7
  36. package/dist/steer.js +115 -10
  37. package/dist/store.d.ts +2 -0
  38. package/dist/store.js +18 -1
  39. package/dist/tools.d.ts +29 -1
  40. package/dist/tools.js +435 -50
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +13 -1
@@ -0,0 +1,173 @@
1
+ import { type ArtefactWeave } from "@miadi/inquiry-weave";
2
+ import { type ReportDigest } from "./research-digest.js";
3
+ /** A saved report may be 1 MB of characters; several times that is not a document anyone reads through a tool. */
4
+ export declare const MAX_SHELF_FILE_BYTES: number;
5
+ export declare class ShelfRefusal extends Error {
6
+ readonly code: "invalid_ref" | "not_found" | "forbidden";
7
+ constructor(code: "invalid_ref" | "not_found" | "forbidden", message: string);
8
+ }
9
+ /** Who may read a folder. `weave` is undefined for a folder no one saved through a door. */
10
+ export declare function canReadShelf(weave: ArtefactWeave | undefined, person: {
11
+ id: string;
12
+ api_tier: string | null;
13
+ }): boolean;
14
+ export interface ShelfReadOptions {
15
+ file?: string;
16
+ offset?: number;
17
+ revision?: string;
18
+ maxBytes?: number;
19
+ }
20
+ /** The folder's manifest, and one file's page when a file is named or the folder holds its report. */
21
+ export declare function readShelfArtefact(root: string, name: string, person: {
22
+ id: string;
23
+ api_tier: string | null;
24
+ }, opts?: ShelfReadOptions): Promise<{
25
+ note: string;
26
+ files_truncated?: string | undefined;
27
+ episodes: string[];
28
+ git: {
29
+ commit?: string;
30
+ committed_at?: string;
31
+ uncommitted_changes?: boolean;
32
+ state: "known" | "unknown";
33
+ };
34
+ files: {
35
+ path: string;
36
+ bytes: number;
37
+ sha256?: string;
38
+ readable: boolean;
39
+ }[];
40
+ saved?: {
41
+ file: string;
42
+ sha256: string;
43
+ bytes: number;
44
+ saved_at: string;
45
+ via: string | undefined;
46
+ refs: string[];
47
+ } | undefined;
48
+ issue?: string | undefined;
49
+ issue_url?: string | undefined;
50
+ circle?: string | undefined;
51
+ visibility: "circle" | "published" | "private" | "proposed";
52
+ owner?: {
53
+ person_id: string;
54
+ name: string | undefined;
55
+ } | undefined;
56
+ artefact: string;
57
+ } | {
58
+ page: {
59
+ offset: number;
60
+ end: number;
61
+ bytes: number;
62
+ sha256: string;
63
+ complete: boolean;
64
+ next: {
65
+ offset: number;
66
+ revision: string;
67
+ } | null;
68
+ };
69
+ text: string;
70
+ modified_at?: string | undefined;
71
+ verification?: string | undefined;
72
+ bytes: number;
73
+ sha256: string;
74
+ verified: boolean;
75
+ file: string;
76
+ content_type: string;
77
+ integrity: string;
78
+ file_git: {
79
+ commit?: string;
80
+ committed_at?: string;
81
+ uncommitted_changes?: boolean;
82
+ state: "known" | "unknown";
83
+ };
84
+ files_truncated?: string | undefined;
85
+ episodes: string[];
86
+ git: {
87
+ commit?: string;
88
+ committed_at?: string;
89
+ uncommitted_changes?: boolean;
90
+ state: "known" | "unknown";
91
+ };
92
+ files: {
93
+ path: string;
94
+ bytes: number;
95
+ sha256?: string;
96
+ readable: boolean;
97
+ }[];
98
+ saved?: {
99
+ file: string;
100
+ sha256: string;
101
+ bytes: number;
102
+ saved_at: string;
103
+ via: string | undefined;
104
+ refs: string[];
105
+ } | undefined;
106
+ issue?: string | undefined;
107
+ issue_url?: string | undefined;
108
+ circle?: string | undefined;
109
+ visibility: "circle" | "published" | "private" | "proposed";
110
+ owner?: {
111
+ person_id: string;
112
+ name: string | undefined;
113
+ } | undefined;
114
+ artefact: string;
115
+ }>;
116
+ /** One thing on the shelf, as its own files say it is: computed from them, never summarised. */
117
+ export interface ShelfListing extends ReportDigest {
118
+ ref: string;
119
+ artefact: string;
120
+ /** `report`: saved through a door, with its hash. `folder`: anything else with a document. `notes`: agent instructions or unreadable files only. */
121
+ kind: "report" | "folder" | "notes";
122
+ /** The document its title and opening come from, the one a reader opens on. */
123
+ main?: string;
124
+ files: number;
125
+ /** Saved, or last woven, or its main document's last change. */
126
+ at?: string;
127
+ owner?: {
128
+ person_id: string;
129
+ name?: string;
130
+ };
131
+ visibility: string;
132
+ circle?: string;
133
+ issue?: string;
134
+ issue_url?: string;
135
+ episodes: {
136
+ number?: number;
137
+ title: string;
138
+ }[];
139
+ saved?: {
140
+ sha256: string;
141
+ bytes: number;
142
+ saved_at: string;
143
+ via?: string;
144
+ refs: string[];
145
+ };
146
+ }
147
+ /** One folder's listing. A folder that cannot be read is listed by its name. */
148
+ export declare function shelfListing(root: string, name: string, weave: ArtefactWeave | undefined): ShelfListing;
149
+ export type ShelfSort = "newest" | "oldest" | "longest" | "title";
150
+ export declare function sortListings(items: ShelfListing[], sort?: ShelfSort): ShelfListing[];
151
+ /**
152
+ * The shelf as this person may read it. `mine` is what they own. `all` is every folder, the write
153
+ * tier's alone, because a folder another person owns or no one owns is theirs to read and no one else's.
154
+ */
155
+ export declare function listShelfArtefacts(root: string, person: {
156
+ id: string;
157
+ api_tier: string | null;
158
+ }, opts?: {
159
+ scope?: "mine" | "all";
160
+ query?: string;
161
+ kind?: ShelfListing["kind"];
162
+ sort?: ShelfSort;
163
+ limit?: number;
164
+ offset?: number;
165
+ }): Promise<{
166
+ scope: "mine" | "all";
167
+ total: number;
168
+ offset: number;
169
+ limit: number;
170
+ items: ShelfListing[];
171
+ next_offset: number | null;
172
+ read_with: string;
173
+ }>;
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Reading what is on the inquiry shelf (jgwill/Miadi#879). save_research and the agents who work
3
+ * on the shelf write here, and this reads it back from a later conversation without another mission.
4
+ *
5
+ * Who may read: the folder's owner, or Miadi's write tier. That is the same rule the shelf's other
6
+ * doors keep, and a folder with no owner recorded is the write tier's alone. No one has defined who
7
+ * sits in a circle for `visibility: circle`, so sharing with a circle admits no one new until that
8
+ * is decided.
9
+ *
10
+ * What can be read: the text files the episode doors read, by their path in the folder. Hidden
11
+ * paths, links, and anything outside the folder are never served. Each answer says whether the
12
+ * bytes are still the bytes saved and which commit holds them.
13
+ */
14
+ import { execFile } from "node:child_process";
15
+ import { createHash } from "node:crypto";
16
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
17
+ import { join, relative, sep } from "node:path";
18
+ import { promisify } from "node:util";
19
+ import { artefactVisibility, readArtefactWeave } from "@miadi/inquiry-weave";
20
+ import { FileRefusal, checkRelativePath, extensionOf, pageOf, TEXT_TYPES } from "./episode-files.js";
21
+ import { digestReport, folderTitle } from "./research-digest.js";
22
+ const run = promisify(execFile);
23
+ const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
24
+ const REPORT_FILE = "report.md";
25
+ /** A saved report may be 1 MB of characters; several times that is not a document anyone reads through a tool. */
26
+ export const MAX_SHELF_FILE_BYTES = 4 * 1024 * 1024;
27
+ const MAX_LISTED_FILES = 200;
28
+ export class ShelfRefusal extends Error {
29
+ code;
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.code = code;
33
+ }
34
+ }
35
+ /** Who may read a folder. `weave` is undefined for a folder no one saved through a door. */
36
+ export function canReadShelf(weave, person) {
37
+ if (person.api_tier === "write")
38
+ return true;
39
+ if (!weave)
40
+ return false;
41
+ return weave.owner?.person_id === person.id;
42
+ }
43
+ function folderOf(root, name) {
44
+ if (!/^[\w.-]+$/.test(name) || name.startsWith("."))
45
+ throw new ShelfRefusal("invalid_ref", "Give the shelf folder's name, inquiry-artefact:<name> or the research_artifact record id.");
46
+ const dir = join(root, name);
47
+ let st;
48
+ try {
49
+ st = lstatSync(dir);
50
+ }
51
+ catch {
52
+ throw new ShelfRefusal("not_found", `No folder ${name} on the inquiry shelf.`);
53
+ }
54
+ if (!st.isDirectory())
55
+ throw new ShelfRefusal("not_found", `No folder ${name} on the inquiry shelf.`);
56
+ return dir;
57
+ }
58
+ function listFiles(dir) {
59
+ const files = [];
60
+ let truncated = false;
61
+ const walk = (at) => {
62
+ for (const entry of readdirSync(at, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
63
+ if (entry.name.startsWith(".") || entry.isSymbolicLink())
64
+ continue;
65
+ const full = join(at, entry.name);
66
+ if (entry.isDirectory()) {
67
+ walk(full);
68
+ continue;
69
+ }
70
+ if (!entry.isFile())
71
+ continue;
72
+ if (files.length >= MAX_LISTED_FILES) {
73
+ truncated = true;
74
+ return;
75
+ }
76
+ const path = relative(dir, full).split(sep).join("/");
77
+ const bytes = lstatSync(full).size;
78
+ const readable = extensionOf(path) in TEXT_TYPES && bytes <= MAX_SHELF_FILE_BYTES;
79
+ files.push({ path, bytes, readable, ...(readable ? { sha256: sha256(readFileSync(full)) } : {}) });
80
+ }
81
+ };
82
+ walk(dir);
83
+ return { files, truncated };
84
+ }
85
+ /** The bytes of one file in the folder, refusing anything that leaves it. */
86
+ function readInside(dir, file) {
87
+ let segments;
88
+ try {
89
+ segments = checkRelativePath(file);
90
+ }
91
+ catch {
92
+ throw new FileRefusal("invalid_path", "Name a file by its path in the folder, as files lists it. Hidden, absolute and '..' paths are not read.");
93
+ }
94
+ if (!(extensionOf(file) in TEXT_TYPES))
95
+ throw new FileRefusal("unsupported", `Only text files are read here (${Object.keys(TEXT_TYPES).join(", ")}).`);
96
+ let walked = dir;
97
+ for (const segment of segments) {
98
+ walked = join(walked, segment);
99
+ let st;
100
+ try {
101
+ st = lstatSync(walked);
102
+ }
103
+ catch {
104
+ throw new ShelfRefusal("not_found", `No file ${file} in that folder.`);
105
+ }
106
+ if (st.isSymbolicLink())
107
+ throw new FileRefusal("invalid_path", `${file} passes through a link; shelf files are read by their own path only.`);
108
+ }
109
+ const final = lstatSync(walked);
110
+ if (!final.isFile())
111
+ throw new ShelfRefusal("not_found", `No file ${file} in that folder.`);
112
+ if (final.size > MAX_SHELF_FILE_BYTES)
113
+ throw new FileRefusal("unsupported", `${file} is ${final.size} bytes, more than the ${MAX_SHELF_FILE_BYTES} this door reads.`);
114
+ if (!realpathSync(walked).startsWith(realpathSync(dir) + sep))
115
+ throw new FileRefusal("invalid_path", `${file} is outside its folder.`);
116
+ return readFileSync(walked);
117
+ }
118
+ /** Which commit last held the folder, and whether what is on disk differs from it. Unknown when git cannot say, never guessed. */
119
+ async function gitStateOf(dir, path = ".") {
120
+ try {
121
+ const log = (await run("git", ["-C", dir, "log", "-1", "--format=%H%n%cI", "--", path], { timeout: 5000 })).stdout.trim().split("\n");
122
+ const status = (await run("git", ["-C", dir, "status", "--porcelain", "--", path], { timeout: 5000 })).stdout;
123
+ return { state: "known", ...(log[0] ? { commit: log[0], committed_at: log[1] } : {}), uncommitted_changes: status.trim().length > 0 };
124
+ }
125
+ catch {
126
+ return { state: "unknown" };
127
+ }
128
+ }
129
+ /** What a folder opens on: its saved report, its README, or its one document. Agent instructions are not what a folder is about. */
130
+ function defaultFile(files, saved) {
131
+ const has = (path) => (path && files.some((f) => f.path === path && f.readable) ? path : undefined);
132
+ const documents = files.filter((f) => f.readable && !/^(AGENTS|CLAUDE)\.md$/i.test(f.path));
133
+ return has(saved) ?? has(REPORT_FILE) ?? has("README.md") ?? (documents.length === 1 ? documents[0].path : files.length === 1 && files[0].readable ? files[0].path : undefined);
134
+ }
135
+ /** The folder's manifest, and one file's page when a file is named or the folder holds its report. */
136
+ export async function readShelfArtefact(root, name, person, opts = {}) {
137
+ const dir = folderOf(root, name);
138
+ const weave = await readArtefactWeave(dir).catch(() => undefined);
139
+ if (!canReadShelf(weave, person)) {
140
+ throw new ShelfRefusal("forbidden", weave ? "That folder belongs to another person. Its owner or a person with Miadi's write tier can read it." : "No owner is recorded for that folder, so only a person with Miadi's write tier can read it.");
141
+ }
142
+ const { files, truncated } = listFiles(dir);
143
+ const manifest = {
144
+ artefact: name,
145
+ ...(weave?.owner ? { owner: { person_id: weave.owner.person_id, name: weave.owner.name } } : {}),
146
+ visibility: artefactVisibility(weave),
147
+ ...(weave?.circle ? { circle: weave.circle } : {}),
148
+ ...(weave?.issue ? { issue: weave.issue, issue_url: weave.issue_url } : {}),
149
+ ...(weave?.source ? { saved: { file: weave.source.file, sha256: weave.source.sha256, bytes: weave.source.bytes, saved_at: weave.source.saved_at, via: weave.source.via, refs: weave.source.refs ?? [] } } : {}),
150
+ episodes: (weave?.episodes ?? []).map((e) => (e.number !== undefined ? `miadi-chronicle://${e.number}` : e.path)),
151
+ git: await gitStateOf(dir),
152
+ files,
153
+ ...(truncated ? { files_truncated: `Only the first ${MAX_LISTED_FILES} files are listed.` } : {}),
154
+ };
155
+ const wanted = opts.file ?? defaultFile(files, weave?.source?.file);
156
+ if (!wanted)
157
+ return { ...manifest, note: "Name a file from files to read it." };
158
+ const bytes = readInside(dir, wanted);
159
+ let content;
160
+ try {
161
+ content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
162
+ }
163
+ catch {
164
+ throw new FileRefusal("unsupported", `${wanted} is not valid UTF-8 text.`);
165
+ }
166
+ const whole = sha256(bytes);
167
+ const page = pageOf({ episode: name, file: wanted, content, size: bytes.byteLength, revision: { sha256: whole, bytes: bytes.byteLength } }, opts);
168
+ const savedHash = weave?.source?.file === wanted ? weave.source.sha256 : undefined;
169
+ return {
170
+ ...manifest,
171
+ file: wanted,
172
+ content_type: TEXT_TYPES[extensionOf(wanted)],
173
+ integrity: savedHash ? (savedHash === whole ? "matches_saved_sha256" : "differs_from_saved_sha256") : "no_saved_sha256",
174
+ // git above is the whole folder; this is the one file read.
175
+ file_git: await gitStateOf(dir, wanted),
176
+ ...page,
177
+ };
178
+ }
179
+ const MAIN_DOCUMENTS = ["report.md", "README.md", "readme.md"];
180
+ const AGENT_NOTES = /^(AGENTS|CLAUDE)\.md$/i;
181
+ /** One folder's listing. A folder that cannot be read is listed by its name. */
182
+ export function shelfListing(root, name, weave) {
183
+ const dir = join(root, name);
184
+ let files = 0;
185
+ const documents = [];
186
+ const walk = (at, depth) => {
187
+ for (const entry of readdirSync(at, { withFileTypes: true })) {
188
+ if (entry.name.startsWith(".") || entry.isSymbolicLink())
189
+ continue;
190
+ const full = join(at, entry.name);
191
+ if (entry.isDirectory()) {
192
+ if (depth < 3)
193
+ walk(full, depth + 1);
194
+ }
195
+ else if (entry.isFile()) {
196
+ files++;
197
+ if (depth === 0 && /\.md$/i.test(entry.name) && !AGENT_NOTES.test(entry.name)) {
198
+ const st = statSync(full);
199
+ documents.push({ path: entry.name, size: st.size, mtime: st.mtimeMs });
200
+ }
201
+ }
202
+ }
203
+ };
204
+ try {
205
+ walk(dir, 0);
206
+ }
207
+ catch {
208
+ // Listed by its name.
209
+ }
210
+ const saved = weave?.source;
211
+ const main = (saved ? documents.find((d) => d.path === saved.file) : undefined) ??
212
+ MAIN_DOCUMENTS.map((n) => documents.find((d) => d.path === n)).find(Boolean) ??
213
+ [...documents].sort((a, b) => b.size - a.size)[0];
214
+ let digest = { title: folderTitle(name), titled: false, words: 0, minutes: 0, headings: [], sections: 0, sources: 0 };
215
+ let words = 0;
216
+ for (const d of documents) {
217
+ if (d.size > MAX_SHELF_FILE_BYTES)
218
+ continue;
219
+ try {
220
+ const read = digestReport(readFileSync(join(dir, d.path), "utf8"), name);
221
+ words += read.words;
222
+ if (d === main)
223
+ digest = read;
224
+ }
225
+ catch {
226
+ // Listed by its name.
227
+ }
228
+ }
229
+ const woven = (weave?.episodes ?? []).map((e) => e.related_at).filter(Boolean).sort().at(-1);
230
+ const at = saved?.saved_at ?? woven ?? (main ? new Date(main.mtime).toISOString() : undefined);
231
+ return {
232
+ ...digest,
233
+ words,
234
+ minutes: Math.max(words ? 1 : 0, Math.round(words / 230)),
235
+ ref: `inquiry-artefact:${name}`,
236
+ artefact: name,
237
+ kind: saved ? "report" : main ? "folder" : "notes",
238
+ ...(main ? { main: main.path } : {}),
239
+ files,
240
+ ...(at ? { at } : {}),
241
+ ...(weave?.owner ? { owner: { person_id: weave.owner.person_id, name: weave.owner.name } } : {}),
242
+ visibility: artefactVisibility(weave),
243
+ ...(weave?.circle ? { circle: weave.circle } : {}),
244
+ ...(weave?.issue ? { issue: weave.issue } : {}),
245
+ ...(weave?.issue_url ? { issue_url: weave.issue_url } : {}),
246
+ episodes: (weave?.episodes ?? []).map((e) => ({
247
+ ...(e.number !== undefined ? { number: e.number } : {}),
248
+ title: folderTitle(String(e.path ?? "").split("/").filter(Boolean).at(-1)?.replace(/^\d{4}-\d{2}-\d{2}-episode-\d+-/, "") ?? ""),
249
+ })),
250
+ ...(saved ? { saved: { sha256: saved.sha256, bytes: saved.bytes, saved_at: saved.saved_at, ...(saved.via ? { via: saved.via } : {}), refs: saved.refs ?? [] } } : {}),
251
+ };
252
+ }
253
+ export function sortListings(items, sort = "newest") {
254
+ const by = {
255
+ newest: (a, b) => (b.at ?? "").localeCompare(a.at ?? ""),
256
+ oldest: (a, b) => (a.at ?? "").localeCompare(b.at ?? ""),
257
+ longest: (a, b) => b.words - a.words,
258
+ title: (a, b) => a.title.localeCompare(b.title),
259
+ };
260
+ return [...items].sort(by[sort]);
261
+ }
262
+ /**
263
+ * The shelf as this person may read it. `mine` is what they own. `all` is every folder, the write
264
+ * tier's alone, because a folder another person owns or no one owns is theirs to read and no one else's.
265
+ */
266
+ export async function listShelfArtefacts(root, person, opts = {}) {
267
+ const scope = opts.scope ?? "mine";
268
+ if (scope === "all" && person.api_tier !== "write")
269
+ throw new ShelfRefusal("forbidden", "Listing the whole shelf needs Miadi's write tier. scope mine lists what you own.");
270
+ const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
271
+ const offset = Math.max(opts.offset ?? 0, 0);
272
+ const q = opts.query?.trim().toLowerCase();
273
+ const items = [];
274
+ for (const entry of existsSync(root) ? readdirSync(root, { withFileTypes: true }) : []) {
275
+ if (!entry.isDirectory() || entry.name.startsWith("."))
276
+ continue;
277
+ const weave = await readArtefactWeave(join(root, entry.name)).catch(() => undefined);
278
+ if (scope === "mine" ? weave?.owner?.person_id !== person.id : !canReadShelf(weave, person))
279
+ continue;
280
+ const item = shelfListing(root, entry.name, weave);
281
+ if (opts.kind && item.kind !== opts.kind)
282
+ continue;
283
+ if (q && ![item.title, item.excerpt, item.artefact, ...item.headings].some((t) => t?.toLowerCase().includes(q)))
284
+ continue;
285
+ items.push(item);
286
+ }
287
+ const sorted = sortListings(items, opts.sort ?? "newest");
288
+ return {
289
+ scope,
290
+ total: sorted.length,
291
+ offset,
292
+ limit,
293
+ items: sorted.slice(offset, offset + limit),
294
+ next_offset: offset + limit < sorted.length ? offset + limit : null,
295
+ read_with: "read_research_artifact takes an item's artefact, and its main file opens first",
296
+ };
297
+ }
@@ -0,0 +1,182 @@
1
+ export declare const STEER_PROTOCOL = 1;
2
+ /** A correction no continuation consumed in a day is stale: the work has moved on. A grant's own expiry comes sooner when it has one. */
3
+ export declare const ENVELOPE_TTL_MS: number;
4
+ export declare const CLAUDE_ADAPTER = "miadi-session-observability 0.1.34 or later";
5
+ export declare const DELIVERY_STATES: readonly ["authorized", "forwarded_to_runtime", "queued_at_runtime", "delivered_to_agent_context", "acknowledged", "behavior_adjusted", "unsupported", "expired", "blocked", "unknown"];
6
+ export type DeliveryState = (typeof DELIVERY_STATES)[number];
7
+ export interface RuntimeDeps {
8
+ root: string;
9
+ claudeDir: string;
10
+ isAlive?: (pid: number) => boolean;
11
+ }
12
+ export declare function steeringRoot(env?: NodeJS.ProcessEnv): string;
13
+ export declare function defaultRuntime(env?: NodeJS.ProcessEnv): RuntimeDeps;
14
+ /** The token the agent names back in its own text: the same rule as the adapter's ackToken. */
15
+ export declare const ackToken: (steerId: string) => string;
16
+ export interface ClaudeSessionRecord {
17
+ pid: number;
18
+ sessionId: string;
19
+ status?: string;
20
+ tmux?: string;
21
+ version?: string;
22
+ }
23
+ /** Claude Code's own record of a running session, `<claudeDir>/sessions/<pid>.json`, if its process lives. */
24
+ export declare function readClaudeSession(deps: RuntimeDeps, sessionId: string): ClaudeSessionRecord | null;
25
+ export type RuntimeRoute = {
26
+ route: "claude_hook_inbox";
27
+ session_id: string;
28
+ seat: string;
29
+ transcript_path?: string;
30
+ } | {
31
+ route: "none";
32
+ code: "unsupported" | "wrong_pane" | "stale_binding";
33
+ reason: string;
34
+ };
35
+ /**
36
+ * Mission → session → runtime, from what each one signed: the runner's receipt names the session,
37
+ * Claude Code's registry says it runs and in which pane, and the adapter's file says this session
38
+ * loaded its hooks. The inbox belongs to this Unix account alone or it is not used.
39
+ */
40
+ export declare function resolveRuntime(deps: RuntimeDeps, binding: {
41
+ session_id?: string;
42
+ pane_id?: string;
43
+ provider?: string;
44
+ }): RuntimeRoute;
45
+ export interface SteerEnvelope {
46
+ protocol: number;
47
+ steer: string;
48
+ grant: string;
49
+ session_id: string;
50
+ instruction: string;
51
+ instruction_sha256: string;
52
+ from: {
53
+ person_id?: string;
54
+ name?: string;
55
+ via?: string;
56
+ };
57
+ queued_at: string;
58
+ expires_at: string;
59
+ transcript_path?: string;
60
+ transcript_offset?: number;
61
+ /** A person decided this exact steer: the grant it cites no longer speaks for it. */
62
+ decided?: boolean;
63
+ }
64
+ /** The steer already on its way with these same words under this grant: one correction, one delivery. */
65
+ export declare function sameInstructionInFlight(deps: RuntimeDeps, sessionId: string, grant: string, instruction: string): string | null;
66
+ /**
67
+ * Put one correction in the session's inbox, durably: fsync, then one rename into pending/, so the
68
+ * adapter never reads half a file. The steer's id keeps it single across a retry or a connector restart.
69
+ */
70
+ export declare function enqueueSteer(deps: RuntimeDeps, args: {
71
+ steer: string;
72
+ grant: string;
73
+ session_id: string;
74
+ instruction: string;
75
+ from: SteerEnvelope["from"];
76
+ grant_expires_at?: string;
77
+ decided?: boolean;
78
+ transcript_path?: string;
79
+ now?: Date;
80
+ }): {
81
+ already: boolean;
82
+ file: string;
83
+ envelope: SteerEnvelope;
84
+ };
85
+ /**
86
+ * Revoke a grant or one steer for this session. The marker stops whatever the adapter would consume
87
+ * next, and what is still pending moves out at once. A rename decides any race with the adapter,
88
+ * so the answer is exact: revoked here, or already consumed.
89
+ */
90
+ export declare function revokeInInbox(deps: RuntimeDeps, sessionId: string, target: {
91
+ grant: string;
92
+ steer?: string;
93
+ }, reason: string): {
94
+ revoked: string[];
95
+ already_consumed: string[];
96
+ };
97
+ export interface DeliveryReceipt {
98
+ state: DeliveryState | "runtime_hook_emitted" | "seat_was_idle" | "seat_woken";
99
+ at?: string;
100
+ source: "connector" | "runtime_inbox" | "runtime_hook" | "claude_transcript" | "connector_clock" | "factory_runner";
101
+ detail?: Record<string, unknown>;
102
+ }
103
+ export interface SteerDelivery {
104
+ state: DeliveryState;
105
+ route: string;
106
+ session_id?: string;
107
+ receipts: DeliveryReceipt[];
108
+ /**
109
+ * While it waits, what will carry it: `next_tool_or_turn_end` for a working seat;
110
+ * `runner_wakes_idle_seat` for a seat already idle, which the factory runner's next pass prompts;
111
+ * `session_not_running` until a person resumes it, or it expires.
112
+ */
113
+ continuation?: "next_tool_or_turn_end" | "runner_wakes_idle_seat" | "session_not_running";
114
+ /** What the agent did after it received it, from its transcript. Whether that follows the correction is a person's judgment unless the steer named what to look for. */
115
+ next_actions?: {
116
+ at?: string;
117
+ tool: string;
118
+ input: string;
119
+ }[];
120
+ behavior?: "not_observed_yet" | "not_judged" | "expected_action_seen" | "expected_action_not_seen";
121
+ note: string;
122
+ }
123
+ interface TranscriptEvidence {
124
+ delivered?: {
125
+ at?: string;
126
+ attachment?: string;
127
+ hook_event?: string;
128
+ tool_use_id?: string;
129
+ };
130
+ acknowledged?: {
131
+ at?: string;
132
+ text: string;
133
+ };
134
+ next_actions: {
135
+ at?: string;
136
+ tool: string;
137
+ input: string;
138
+ }[];
139
+ actions_seen: number;
140
+ raw_inputs: string[];
141
+ }
142
+ /**
143
+ * What Claude Code itself recorded about this steer. Only an attachment naming the steer's id shows
144
+ * it entered the model's context, and only the agent's own later text can acknowledge it. Tool
145
+ * results and the agent's quotes of the id do not count as delivery.
146
+ */
147
+ export declare function readTranscriptEvidence(path: string, offset: number, steer: string): TranscriptEvidence;
148
+ /** Whether a prompt typed into the bound pane reached the session: Claude Code's own user or queued-prompt record. A pane echo proves nothing. */
149
+ export declare function typedLineInTranscript(path: string, offset: number, instruction: string): {
150
+ at?: string;
151
+ record: string;
152
+ } | null;
153
+ /** Find a session's transcript when its adapter did not name it. */
154
+ export declare function transcriptPathOf(deps: RuntimeDeps, sessionId: string): string | undefined;
155
+ /**
156
+ * Where one steer stands, each state with its source and time, for request_session_steer,
157
+ * mission_status and get_run_result. It only reads, and it can be read at any time: ChatGPT
158
+ * disconnecting or the connector restarting changes nothing here.
159
+ */
160
+ export declare function readSteerDelivery(deps: RuntimeDeps, record: {
161
+ id: string;
162
+ refs?: string[];
163
+ body: Record<string, unknown>;
164
+ completion_receipt?: Record<string, unknown> | undefined;
165
+ }, now?: Date): SteerDelivery | null;
166
+ /**
167
+ * What a forwarded steer's ledger record should now say, from its delivery: done once the model has
168
+ * it, stopped once it never will. Null while it is still on its way, or if it has already settled.
169
+ */
170
+ export declare function settlementOf(deps: RuntimeDeps, record: {
171
+ id: string;
172
+ state?: string;
173
+ refs?: string[];
174
+ body: Record<string, unknown>;
175
+ completion_receipt?: Record<string, unknown> | undefined;
176
+ }, now?: Date): {
177
+ state: "completed" | "blocked";
178
+ outcome: DeliveryState;
179
+ message: string;
180
+ at?: string;
181
+ } | null;
182
+ export {};