@miadi/connector 0.1.19 → 0.1.20

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.
package/dist/tools.js CHANGED
@@ -16,17 +16,22 @@ import { reviewIdOf, videoIdOf } from "./reviews.js";
16
16
  import { SCOPE_APPROVE, SCOPE_CONTRIBUTE } from "./oauth.js";
17
17
  import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current, newRecordId, } from "./records.js";
18
18
  import { VERSION } from "./version.js";
19
- import { AlreadyDecided, DECISIONS, DecisionConflict, KIND_LABELS, STILL_RUNNING, StaleVersion, approvalStateOf, consequenceOf, decide, decisionWaitOf, isApprovable, isBeingDecided, logDecision, readQueueState, requestProjection, settleWithin, } from "./approve.js";
19
+ import { AlreadyDecided, DECISIONS, DecisionConflict, KIND_LABELS, ProviderChoiceRequired, STILL_RUNNING, StaleVersion, approvalStateOf, consequenceOf, decide, decisionWaitOf, isApprovable, isBeingDecided, logDecision, readQueueState, requestProjection, settleWithin, } from "./approve.js";
20
20
  import { RevisionRefused, requestHistory, revisionRecord, supersedeRecords, versionOf } from "./revisions.js";
21
21
  import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
22
22
  import { discoverProviders, jevConfigOf, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
23
23
  import { authorizesActions, missionStatus, primaryIssueOf, resolveMissionSubject } from "./mission-status.js";
24
+ import { authorizationReceipt, authorizeProvider, CHOICE_PROVIDERS, isPersonId, needsPersonChoice, providerPolicyOf } from "./provider-policy.js";
24
25
  import { candidatesFor, CRITICALITIES, jevAdapter, modelPreflight, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, TASK_TYPES, } from "./model-preflight.js";
25
26
  import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steerSession } from "./steer.js";
26
27
  import { factoryLedgerPath, readFactoryReceipts, receiptsForRecord, recordsForSession } from "./factory-receipt.js";
27
28
  import { missionResult, redactResult } from "./mission-result.js";
28
29
  import { factoryDir as defaultFactoryDir } from "./queue-state.js";
29
30
  import { join } from "node:path";
31
+ import { readingScope, canonicalVideo, suppliedDialogue } from "./reading/contract.js";
32
+ import { workflowId } from "./reading/workflow.js";
33
+ import { readingTask } from "./reading/adapters.js";
34
+ import { canReadReading, readingOwner } from "./reading/privacy.js";
30
35
  import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
31
36
  import { ResearchUploads, UploadRefusal } from "./uploads.js";
32
37
  import { DEFAULT_PAGE_BYTES, FOUNDATIONS_PERMISSION, FileRefusal, MAX_TEXT_BYTES, MIN_PAGE_BYTES, TEXT_TYPES, checkRelativePath, episodeNumberOf, extensionOf, fileRef, holdsFoundationRead, isFoundationDoc, pageOf, parseFileRef, } from "./episode-files.js";
@@ -179,7 +184,14 @@ export function registerTools(server, ctx) {
179
184
  const redact = (r) => {
180
185
  if (!viewer || viewer.api_tier === "write" || r.actor.person_id === viewer.id)
181
186
  return r;
187
+ if (r.body.reading && r.kind === "observation") {
188
+ const stage = r.body.reading;
189
+ return { ...r, refs: [], evidence_refs: [], body: { reading: { workflow_id: stage.workflow_id, stage: stage.stage, status: stage.status, sha256: stage.sha256, visibility: "source_owner_or_factory_operator" } } };
190
+ }
182
191
  const body = { ...r.body };
192
+ // Grants contain source rights and locators; only the owner and operators inspect them.
193
+ if (body.reading)
194
+ body.reading = { visibility: "source_owner_or_factory_operator" };
183
195
  // A steer's binding, pane, target and artifact path name the seat as the session does (#922 R2).
184
196
  for (const k of ["session", "cwd", "session_id", "binding", "pane"])
185
197
  delete body[k];
@@ -222,7 +234,47 @@ export function registerTools(server, ctx) {
222
234
  };
223
235
  /** Every record, folded; for logic. What a tool returns goes through `shown`. */
224
236
  const all = () => current(ledger.all());
225
- const records = () => all().map(redact);
237
+ const records = () => {
238
+ const raw = all();
239
+ // Completion results can contain exact drafts too. Hide their parent mission as
240
+ // well, so missionStatus/get_run_result cannot rehydrate it from trusted records.
241
+ return raw.filter(r => !viewer || canReadReading(r, raw, viewer)).map(r => {
242
+ // The source owner can read a factory-authored result for their private mission.
243
+ if (readingOwner(r, raw) === viewer?.id)
244
+ return r;
245
+ return redact(r);
246
+ });
247
+ };
248
+ /** Which provider a run may launch with and who authorized it, as every surface shows it (jgwill/Miadi#926). */
249
+ const providerAuthorizationOf = (r) => r && r.kind === "run_request" ? authorizeProvider(r, ledger.all(), { requireFresh: r.approval_state === "awaiting_approval" }) : undefined;
250
+ const authorizationOut = (a) => ({
251
+ ...authorizationReceipt(a),
252
+ explanation: a.explanation,
253
+ ...(a.not_binding ? { not_binding: a.not_binding } : {}),
254
+ });
255
+ /** A mission's status with the authorization of its provider, and a note when the provider that ran was never authorized. */
256
+ const withProviderAuthorization = (status) => {
257
+ const subject = status?.subject?.id ? all().find((r) => r.id === status.subject.id) : undefined;
258
+ const a = providerAuthorizationOf(subject);
259
+ if (!a)
260
+ return status;
261
+ // An approval records the authorization it was given; a run approved before jgwill/Miadi#926 recorded none.
262
+ const given = subject?.completion_receipt?.provider_authorization;
263
+ const actual = typeof status.provider?.actual === "string" ? status.provider.actual : null;
264
+ const unauthorizedLaunch = Boolean(actual && needsPersonChoice(actual) && !a.authorized && !given?.authorized);
265
+ return {
266
+ ...status,
267
+ provider_authorization: { ...authorizationOut(a), ...(given ? { at_approval: given } : {}), ...(unauthorizedLaunch ? { launched_without_authorization: true } : {}) },
268
+ ...(unauthorizedLaunch
269
+ ? {
270
+ notes: [
271
+ ...(Array.isArray(status.notes) ? status.notes : []),
272
+ `${actual} ran for this mission without ${a.requester.name}'s own choice for it on record (jgwill/Miadi#926). Nothing was stopped or re-run because of it.`,
273
+ ],
274
+ }
275
+ : {}),
276
+ };
277
+ };
226
278
  const uploads = new ResearchUploads(join(config.stateDir, "uploads"));
227
279
  const touching = (ref) => records().filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref) || r.id === ref);
228
280
  /** The factory runner's delivery receipts, read fresh: operator data, for the write tier only. */
@@ -248,6 +300,13 @@ export function registerTools(server, ctx) {
248
300
  service: "@miadi/connector",
249
301
  version: VERSION,
250
302
  contract: "jgwill/Miadi#815 Connector v2, jgwill/Miadi#816 Review Bridge",
303
+ reading_workflow: {
304
+ contract: "transcript-first-v1",
305
+ provider_runtime: process.env.MIADI_READING_ADAPTER ? "host_configured_not_verified" : "not_configured",
306
+ transcript_store: "private connector ledger source-part receipts before generation",
307
+ import: "blocked_until_authenticated_sha256_cas_v1_handshake",
308
+ publication: "separate_exact_candidate_approval",
309
+ },
251
310
  signed_in_as: { person_id: person.id, name: person.name, role: person.role ?? null, miadi_api_tier: person.api_tier, via: auth.clientId },
252
311
  scopes: auth.scopes,
253
312
  can_contribute: auth.scopes.includes(SCOPE_CONTRIBUTE),
@@ -732,7 +791,7 @@ export function registerTools(server, ctx) {
732
791
  }, wrap(async ({ ref }, extra) => {
733
792
  const { person } = who(extra);
734
793
  const writeTier = person.api_tier === "write";
735
- return missionStatus(ref.trim(), records(), {
794
+ return withProviderAuthorization(await missionStatus(ref.trim(), records(), {
736
795
  projectOf: (r) => requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
737
796
  queueOf: (r) => (r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined),
738
797
  // Without a lister the host cannot say which sessions live: unknown, never "absent" (jgwill/Miadi#909).
@@ -788,11 +847,11 @@ export function registerTools(server, ctx) {
788
847
  return { state: i.state, title: i.title };
789
848
  },
790
849
  liveVersionOf: (pkg) => (pkg === "@miadi/connector" ? VERSION : undefined),
791
- });
850
+ }));
792
851
  }));
793
852
  server.registerTool("provider_capabilities", {
794
853
  title: "Which agent providers this host can launch",
795
- description: "The provider reality of the connector host (jgwill/Miadi#893): which agent CLIs are installed, their versions, whether sign-in evidence exists, the preference order (Codex, Pi, Copilot; Claude Code only on explicit request), and the read-only Jev routing preflight. No token is read or shown. Needs Miadi's write tier: this is host truth.",
854
+ description: "The provider reality of the connector host (jgwill/Miadi#893): which agent CLIs are installed, their versions, whether sign-in evidence exists, the preference order (Pi, Copilot by default; Codex and Claude Code only when a request names them), the rule that a named Codex runs only on the requester's own choice for that mission or their own policy (jgwill/Miadi#926), your own provider policy, and the read-only Jev routing preflight. No token is read or shown. Needs Miadi's write tier: this is host truth.",
796
855
  inputSchema: {},
797
856
  annotations: READ,
798
857
  }, wrap(async (_args, extra) => {
@@ -801,9 +860,12 @@ export function registerTools(server, ctx) {
801
860
  return {
802
861
  preference_order: PROVIDERS.map((p) => p.id),
803
862
  explicit_only: PROVIDERS.filter((p) => !p.selectable_by_default).map((p) => p.id),
863
+ needs_person_choice: [...CHOICE_PROVIDERS],
864
+ your_policy: providerPolicyOf(ledger.all(), person.id),
865
+ policy_page: `${config.publicUrl}/providers`,
804
866
  providers: discoverProviders(ctx.providersProbe),
805
867
  router: await routerPreflight(process.env, ctx.approval?.fetchImpl ?? fetch),
806
- note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted.",
868
+ note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted. A request may name Codex, but it runs only when its requester chooses Codex for that mission on the approval page, or their own policy on the providers page says so; a generic approval, the assistant's provider field, the host's order or Jev never choose it.",
807
869
  };
808
870
  }));
809
871
  server.registerTool("mission_model_preflight", {
@@ -842,7 +904,8 @@ export function registerTools(server, ctx) {
842
904
  const body = record.body;
843
905
  const provider = (typeof body.resolved_provider === "string" ? body.resolved_provider : typeof body.provider === "string" ? body.provider : null);
844
906
  const basis = record.approval_state === "approved" ? "approved" : "requested";
845
- const candidates = candidatesFor(discoverProviders(ctx.providersProbe), provider, ctx.modelOf, basis);
907
+ const authorization = providerAuthorizationOf(record);
908
+ const candidates = candidatesFor(discoverProviders(ctx.providersProbe), provider, ctx.modelOf, basis, authorization);
846
909
  const jevConfig = jevConfigOf(process.env);
847
910
  const jev = a.consult_jev ? (ctx.jevAdapter ?? (jevConfig ? jevAdapter(jevConfig, ctx.approval?.fetchImpl ?? fetch) : undefined)) : undefined;
848
911
  const configured = Boolean(jevConfig);
@@ -859,6 +922,7 @@ export function registerTools(server, ctx) {
859
922
  task: a.task_type,
860
923
  criticality: a.criticality,
861
924
  candidates,
925
+ ...(authorization ? { provider_authorization: authorizationOut(authorization) } : {}),
862
926
  ...(a.launch ? { launch: a.launch } : {}),
863
927
  ...(jev ? { adapter: jev } : { routerWhenAbsent: configured ? "not_requested" : "router_unconfigured" }),
864
928
  });
@@ -1064,6 +1128,17 @@ export function registerTools(server, ctx) {
1064
1128
  const video = videoIdOf(a.video);
1065
1129
  if (!video)
1066
1130
  throw new ToolRefusal("invalid_ref", "Give a YouTube URL or an 11-character video id.");
1131
+ const prior = all().find(r => r.kind === "source_intent" && r.actor.person_id === person.id && r.idempotency_key === `${video}:${a.purpose}`);
1132
+ if (prior && prior.body.readiness !== a.readiness) {
1133
+ // Release/withdrawal is an explicit source-owner contribution, not processing authority.
1134
+ const transition = {
1135
+ id: newRecordId("source_intent"), kind: "source_intent", created_at: new Date().toISOString(), actor: actorOf(person, auth), supersedes: prior.id,
1136
+ refs: prior.refs, evidence_refs: [], state: a.readiness === "released_for_review" ? "ready" : a.readiness === "withheld" ? "blocked" : "pending",
1137
+ body: { ...prior.body, readiness: a.readiness, source_readiness_transition: true },
1138
+ };
1139
+ ledger.appendDurable(transition);
1140
+ return all().find(r => r.id === prior.id);
1141
+ }
1067
1142
  return append("source_intent", person, auth, {
1068
1143
  circle: "continuity",
1069
1144
  refs: [...a.refs, `https://youtu.be/${video}`],
@@ -1281,6 +1356,55 @@ export function registerTools(server, ctx) {
1281
1356
  };
1282
1357
  }));
1283
1358
  // ── requests: recorded, approved by a person on the approval page, then run ──
1359
+ server.registerTool("request_screenwalk_reading", {
1360
+ title: "Release a source for a bounded research reading",
1361
+ description: "One approved run archives the released source transcript before generating V1, an academic survey and a labelled reflective reading. It stops at the exact candidate's editorial gate. No paid fallback, public release, Deep Diver or Chronicle is included. A host runtime adapter must be configured; unavailable providers are reported as blockers.",
1362
+ inputSchema: { scope: readingScope, companion: suppliedDialogue.optional(), ...base },
1363
+ annotations: WRITE,
1364
+ }, wrap(async (a, extra) => {
1365
+ const { person, auth } = who(extra);
1366
+ requireContribute(auth);
1367
+ const scope = readingScope.parse(a.scope);
1368
+ const source = all().find(r => r.id === scope.source_intent);
1369
+ if (!source || source.kind !== "source_intent" || source.actor.person_id !== person.id || source.state !== "ready" || source.body.readiness !== "released_for_review")
1370
+ throw new ToolRefusal("source_not_released", "The source owner must explicitly register this source as released_for_review before requesting processing.");
1371
+ const video = canonicalVideo(String(source.body.video_id));
1372
+ const id = workflowId(person.id, video, scope.revision_of);
1373
+ if (scope.revision_of) {
1374
+ const prior = ledger.all().find(r => r.kind === "observation" && r.body.reading?.workflow_id === scope.revision_of && r.body.reading?.stage === "registration");
1375
+ const priorRequest = all().find(r => r.kind === "run_request" && r.body.workflow_id === scope.revision_of && r.actor.person_id === person.id && r.approval_state === "approved" && r.body.reading?.source_intent === scope.source_intent);
1376
+ if ((!prior && !priorRequest) || (prior && (prior.actor.person_id !== person.id || prior.body.reading?.video_id !== video)))
1377
+ throw new ToolRefusal("invalid_revision", "A revision must name your prior workflow for the same video.");
1378
+ }
1379
+ const existing = all().find(r => r.kind === "run_request" && r.actor.person_id === person.id && r.body.workflow_id === id);
1380
+ if (existing)
1381
+ return { ...redact(existing), workflow_id: id, approval_url: approvalUrl(existing.id), replayed: true, note: "This video's bounded request already exists; read or resume it. The new scope was not substituted." };
1382
+ const resolution = resolveProvider(undefined, discoverProviders(ctx.providersProbe));
1383
+ if (resolution.state !== "ok")
1384
+ return { state: resolution.state, note: "No runtime provider was verified; nothing was queued." };
1385
+ const r = append("run_request", person, auth, {
1386
+ circle: "engineering", refs: [source.id, `https://youtu.be/${video}`], intention: a.intention,
1387
+ idempotency_key: id, approval_state: "awaiting_approval", state: "pending",
1388
+ body: { task: readingTask(scope), reading: scope, ...(a.companion ? { reading_companion: a.companion } : {}), session: `reading-${id.slice(8)}`, workflow_id: id, resolved_provider: resolution.provider.id },
1389
+ });
1390
+ return { ...r, workflow_id: id, approval_url: approvalUrl(r.id), gates: ["editorial_public_import", "deep_diver", "chronicle", "cultural_reuse"] };
1391
+ }));
1392
+ server.registerTool("request_reading_import", {
1393
+ title: "Approve the exact staged reading for public import",
1394
+ description: "Request a separate editorial decision for one candidate hash and its exact base. Import is a public release; it refuses legacy unconditional writes. Does not authorize Deep Diver, Chronicle or further reuse.",
1395
+ inputSchema: { workflow_id: z.string().regex(/^reading:[a-f\d]{32}$/), sha256: z.string().regex(/^[a-f\d]{64}$/), expected_version: z.number().int().min(0), expected_sha256: z.string().regex(/^[a-f\d]{64}$/), public_release: z.literal(true), ...base },
1396
+ annotations: WRITE,
1397
+ }, wrap(async (a, extra) => {
1398
+ const { person, auth } = who(extra);
1399
+ requireContribute(auth);
1400
+ // Read from the existing ledger, without constructing a second workflow store.
1401
+ const candidates = ledger.all().filter(r => r.kind === "observation" && r.body.reading?.workflow_id === a.workflow_id && r.body.reading?.stage === "v3" && r.body.reading?.status === "completed");
1402
+ const candidate = candidates[0]?.body.reading?.artifact;
1403
+ if (!candidate || candidates[0].actor.person_id !== person.id || candidate.sha256 !== a.sha256 || candidate.expected_version !== a.expected_version || candidate.expected_sha256 !== a.expected_sha256)
1404
+ throw new ToolRefusal("candidate_mismatch", "Read this person's staged candidate and request its exact hash and base.");
1405
+ const r = append("run_request", person, auth, { circle: "story", refs: [a.workflow_id], intention: a.intention, idempotency_key: `${a.workflow_id}:import:${a.sha256}`, state: "pending", approval_state: "awaiting_approval", body: { task: "Run node packages/connector/scripts/reading.mjs import <this-run-request-id>. Only import the exact typed reading_import candidate after the service proves atomic CAS and durable idempotency; read it back exactly. Stop on any mismatch. No Deep Diver or Chronicle.", reading_import: { workflow_id: a.workflow_id, sha256: a.sha256, expected_version: a.expected_version, expected_sha256: a.expected_sha256, public_release: true }, reading_preview: { markdown: candidate.markdown, diffs: candidate.diffs, manifest: candidate.manifest } } });
1406
+ return { ...r, approval_url: approvalUrl(r.id), candidate: { sha256: candidate.sha256, bytes: candidate.bytes, diff: candidate.diffs, manifest: candidate.manifest } };
1407
+ }));
1284
1408
  server.registerTool("request_authorized_run", {
1285
1409
  title: "Request a bounded agent run",
1286
1410
  description: "Ask for an agent session to do one bounded task. The request waits for a person with Miadi's write tier to approve it on the returned approval_url; once approved, a brief is queued for the factory intake, which opens or reaches the session within minutes. Read it back with get_run_result. This is for a new mission only: to change a request that still awaits a decision, call revise_pending_request with its id, so the person keeps one card to decide.",
@@ -1294,7 +1418,7 @@ export function registerTools(server, ctx) {
1294
1418
  provider: z
1295
1419
  .enum(["codex", "pi", "copilot", "claude"])
1296
1420
  .optional()
1297
- .describe("The provider the run should launch with, verified installed on the connector host. Claude Code runs only when named explicitly. Unknown or unavailable leaves the mission unstarted."),
1421
+ .describe("The provider the run should launch with, verified installed on the connector host. Name it only when the person asked for that provider for this mission. Claude Code runs only when named. Codex runs only when the person also chooses Codex for this mission on the approval page (or their own policy allows it): naming it here does not choose it for them (jgwill/Miadi#926). Unknown or unavailable leaves the mission unstarted, and nothing falls back to Codex."),
1298
1422
  steering: z
1299
1423
  .object({
1300
1424
  max_attempts: z.number().int().min(1).max(20).describe("How many authorized follow-up steers this approval covers."),
@@ -1334,10 +1458,25 @@ export function registerTools(server, ctx) {
1334
1458
  ...(a.steering ? { steering: a.steering } : {}),
1335
1459
  },
1336
1460
  });
1461
+ const authorization = providerAuthorizationOf(all().find((x) => x.id === r.id) ?? r);
1337
1462
  return {
1338
1463
  ...r,
1339
- provider: { requested: a.provider ?? null, resolved: resolution.provider.id, explicit: resolution.explicit, version: resolution.provider.version },
1464
+ provider: {
1465
+ requested: a.provider ?? null,
1466
+ resolved: resolution.provider.id,
1467
+ explicit: resolution.explicit,
1468
+ version: resolution.provider.version,
1469
+ authorization: authorizationOut(authorization),
1470
+ },
1340
1471
  approval_url: approvalUrl(r.id),
1472
+ ...(authorization.authorized
1473
+ ? {}
1474
+ : {
1475
+ // #926 review R4, RR5: a provider is revised to another one, never cleared; a service token cannot choose at all.
1476
+ next_action: isPersonId(person.id)
1477
+ ? `Tell ${person.name} that a Codex run needs their own choice on the approval page (${approvalUrl(r.id)}); approving alone, on the page or a card, will not run it. Do not choose for them. If they did not ask for Codex for this mission, revise the request with another provider (pi, copilot or claude).`
1478
+ : `A request made with a service token cannot run Codex: only a person chooses it, for a request of their own. Revise it with another provider (pi, copilot or claude).`,
1479
+ }),
1341
1480
  };
1342
1481
  }));
1343
1482
  // ── one mission, many versions, one decision (jgwill/Miadi#919) ────────────
@@ -1435,10 +1574,13 @@ export function registerTools(server, ctx) {
1435
1574
  throw error;
1436
1575
  }
1437
1576
  ledger.append(record);
1577
+ // A revision moves the version: a provider choice made for an earlier version no longer binds (jgwill/Miadi#926).
1578
+ const revisedAuthorization = providerAuthorizationOf(all().find((x) => x.id === (record.supersedes ?? record.id)));
1438
1579
  return {
1439
1580
  ...revisionOut(record.supersedes ? { ...record, id: record.supersedes } : record),
1440
1581
  revised: record.revision,
1441
1582
  ...(provider ? { provider } : {}),
1583
+ ...(revisedAuthorization ? { provider_authorization: authorizationOut(revisedAuthorization) } : {}),
1442
1584
  note: `One request, one approval: its page and card now show version ${record.revision.version}. Earlier versions stay readable with get_request_history.`,
1443
1585
  };
1444
1586
  }));
@@ -1878,8 +2020,9 @@ export function registerTools(server, ctx) {
1878
2020
  created_at: r.created_at,
1879
2021
  circle: r.circle,
1880
2022
  intention: r.intention,
1881
- consequence: consequenceOf(r),
2023
+ consequence: consequenceOf(r, providerAuthorizationOf(r)),
1882
2024
  detail: detailOf(r),
2025
+ ...(r.kind === "run_request" ? { provider_authorization: authorizationOut(providerAuthorizationOf(r)) } : {}),
1883
2026
  approval_url: approvalUrl(r.id),
1884
2027
  projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
1885
2028
  }));
@@ -1972,6 +2115,13 @@ export function registerTools(server, ctx) {
1972
2115
  decided = settled;
1973
2116
  }
1974
2117
  catch (error) {
2118
+ // No projection here: the card shows this message, and the request still awaits a decision (jgwill/Miadi#926).
2119
+ if (error instanceof ProviderChoiceRequired) {
2120
+ throw new ToolRefusal("provider_choice_required", `${error.message} Open its approval page: ${approvalUrl(r.id)}`, {
2121
+ approval_url: approvalUrl(r.id),
2122
+ provider_authorization: authorizationOut(error.authorization),
2123
+ });
2124
+ }
1975
2125
  if (error instanceof StaleVersion)
1976
2126
  throw new ToolRefusal("stale_version", error.message, { projection: projectionNow(), approval_url: approvalUrl(r.id) });
1977
2127
  // A different decision recorded a moment earlier, by another click or client, is reported with where it stands.
@@ -2086,6 +2236,8 @@ export const TOOL_STATUS = {
2086
2236
  supersede_pending_requests: "live, contribute (retires named earlier requests still waiting and joins them as earlier versions of the one that stands)",
2087
2237
  request_session_steer: "live, contribute, write tier (runs inside an approved run's steering grant, else one scoped human decision; get_capabilities.steering says whether the transport works now)",
2088
2238
  request_review_operation: "live, contribute, needs_approval",
2239
+ request_screenwalk_reading: "live, contribute, needs_approval (host provider adapter required; stops at editorial decision)",
2240
+ request_reading_import: "live, contribute, needs_approval (exact public import; atomic service contract required)",
2089
2241
  request_deep_diver: "live, contribute, needs_approval",
2090
2242
  link_review_to_episode: "live, contribute, needs_approval",
2091
2243
  save_research: "live, contribute (the whole report, in parts when long; saved on the inquiry shelf with its issue, pushed; private by default)",
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.19";
1
+ export declare const VERSION = "0.1.20";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.19";
1
+ export const VERSION = "0.1.20";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/connector",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "The Miadi connector: one remote MCP service through which a person signed in with their Miadi person token, from ChatGPT or any MCP client, reads sessions, reviews, episodes and circles and records observations, inquiries, handoffs and proposals with their name on each record.",
5
5
  "license": "MIT",
6
6
  "author": "Miadi",