@botiverse/raft-sdk 0.4.0 → 0.5.0

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.
@@ -4562,6 +4562,7 @@ const AGENT_API_ROUTE_META = {
4562
4562
  integrationAppList: read,
4563
4563
  integrationAppStatus: read,
4564
4564
  actionPrepare: write,
4565
+ agentCommand: write,
4565
4566
  attachmentUpload: write,
4566
4567
  attachmentUploadCapabilities: read,
4567
4568
  attachmentUploadSessionCreate: write,
@@ -5465,6 +5466,47 @@ const agentApiActionPrepareBodySchema = passthroughObject({
5465
5466
  target: string().trim().min(1),
5466
5467
  action: actionCardActionSchema
5467
5468
  });
5469
+ /** Most UTF-8 bytes all argv items together may carry. */
5470
+ const AGENT_API_COMMAND_ARGV_MAX_BYTES = 16384;
5471
+ /** Most characters of `stdin`: the Server's message body cap (MAX_MESSAGE_LENGTH). */
5472
+ const AGENT_API_COMMAND_STDIN_MAX_LENGTH = 32e3;
5473
+ function isIanaTimeZone(value) {
5474
+ try {
5475
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
5476
+ return true;
5477
+ } catch {
5478
+ return false;
5479
+ }
5480
+ }
5481
+ const agentApiCommandBodySchema = passthroughObject({
5482
+ argv: array(string()).max(64).refine((argv) => argv.reduce((total, item) => total + new TextEncoder().encode(item).length, 0) <= AGENT_API_COMMAND_ARGV_MAX_BYTES, { message: `argv must not exceed ${AGENT_API_COMMAND_ARGV_MAX_BYTES} bytes in total` }),
5483
+ stdin: string().max(AGENT_API_COMMAND_STDIN_MAX_LENGTH).optional(),
5484
+ origin: _enum(["model", "code"]).optional(),
5485
+ idempotencyKey: string().trim().min(1).max(200).optional(),
5486
+ timezone: string().trim().min(1).max(64).refine(isIanaTimeZone, { message: "timezone must be an IANA time zone" }).optional(),
5487
+ /**
5488
+ * The model's current context (opaque). Reads are booked under it and a send
5489
+ * only attests reads from the same context, so a gateway changes it when the
5490
+ * model's context no longer holds what it read (new session, compaction),
5491
+ * not per turn. Absent: reads count in any context.
5492
+ */
5493
+ contextId: string().trim().min(1).max(200).optional(),
5494
+ /**
5495
+ * `message check` acknowledgement. A hosted check returns the batch it
5496
+ * handed out unacknowledged, with `outcome.data.eventsCursor`; the gateway
5497
+ * sends that value back here on a later request only after that result
5498
+ * reached the model, and the next check then acknowledges the batch.
5499
+ * Absent, or any value other than the cursor of the last batch handed out
5500
+ * (a retry carrying an older cursor included), acknowledges nothing: the
5501
+ * unacknowledged batch is delivered again.
5502
+ */
5503
+ ackEventsCursor: string().trim().min(1).max(64).optional()
5504
+ });
5505
+ const agentApiCommandResponseSchema = passthroughObject({
5506
+ exitCode: number$1().int(),
5507
+ text: string(),
5508
+ outcome: record(string(), unknown()).nullable()
5509
+ });
5468
5510
  const agentApiServerUpdateBodySchema = passthroughObject({
5469
5511
  name: string().trim().min(1).max(100).optional(),
5470
5512
  hideHumansFromMembers: boolean().optional()
@@ -7046,6 +7088,19 @@ const agentApiContract = {
7046
7088
  request: { body: agentApiActionPrepareBodySchema },
7047
7089
  response: { body: agentApiActionPrepareResponseSchema }
7048
7090
  }),
7091
+ agentCommand: route({
7092
+ key: "agentCommand",
7093
+ method: "POST",
7094
+ path: "/command",
7095
+ client: {
7096
+ resource: "commands",
7097
+ method: "run"
7098
+ },
7099
+ capability: "read",
7100
+ description: "Run one `raft` command as the bound agent and return the text the CLI prints, its exit code and its outcome. HTTP 200 whenever the command ran, failed commands included; each request the command makes is authorized on its own.",
7101
+ request: { body: agentApiCommandBodySchema },
7102
+ response: { body: agentApiCommandResponseSchema }
7103
+ }),
7049
7104
  attachmentUpload: route({
7050
7105
  key: "attachmentUpload",
7051
7106
  method: "POST",
@@ -9357,6 +9412,31 @@ const AGENT_API_ROUTE_MANIFEST = [
9357
9412
  "body": true
9358
9413
  }
9359
9414
  },
9415
+ {
9416
+ "key": "agentCommand",
9417
+ "method": "POST",
9418
+ "path": "/command",
9419
+ "fullPath": "/internal/agent-api/command",
9420
+ "client": {
9421
+ "resource": "commands",
9422
+ "method": "run"
9423
+ },
9424
+ "capability": "read",
9425
+ "description": "Run one `raft` command as the bound agent and return the text the CLI prints, its exit code and its outcome. HTTP 200 whenever the command ran, failed commands included; each request the command makes is authorized on its own.",
9426
+ "sideEffect": "write",
9427
+ "idempotency": "none",
9428
+ "destructive": false,
9429
+ "audience": "both",
9430
+ "request": {
9431
+ "params": false,
9432
+ "query": false,
9433
+ "body": true
9434
+ },
9435
+ "response": {
9436
+ "kind": "json",
9437
+ "body": true
9438
+ }
9439
+ },
9360
9440
  {
9361
9441
  "key": "attachmentUpload",
9362
9442
  "method": "POST",
@@ -9659,7 +9739,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9659
9739
  }
9660
9740
  ];
9661
9741
  /** Content hash of AGENT_API_ROUTE_MANIFEST; see computeAgentApiManifestVersion. */
9662
- const AGENT_API_MANIFEST_VERSION = "c7a793816a7a90ed";
9742
+ const AGENT_API_MANIFEST_VERSION = "311047956109765f";
9663
9743
  //#endregion
9664
9744
  //#region src/routes.ts
9665
9745
  const ROUTE_INFO = Object.fromEntries(AGENT_API_ROUTE_MANIFEST.map((entry) => {
@@ -9929,613 +10009,288 @@ function createRaftManageClient(readApi, writeApi, avatar) {
9929
10009
  };
9930
10010
  }
9931
10011
  //#endregion
9932
- //#region src/client.ts
9933
- var RaftSdkConfigurationError = class extends Error {
9934
- code;
9935
- name = "RaftSdkConfigurationError";
9936
- constructor(code, message) {
9937
- super(message);
9938
- this.code = code;
9939
- }
10012
+ //#region ../shared/src/agentOps/outcome.ts
10013
+ const SERVER_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
10014
+ const DEFAULT_MESSAGES = {
10015
+ INVALID_REQUEST: "The request did not match the Agent API contract; nothing was sent.",
10016
+ TRANSPORT_ERROR: "The request did not reach the Raft Server.",
10017
+ HTTP_ERROR: "The Raft Server rejected the request.",
10018
+ INVALID_RESPONSE: "The Raft Server response did not match the Agent API contract.",
10019
+ CAPABILITY_NOT_AUTHORIZED: "This credential does not carry the capability this operation needs.",
10020
+ SCOPE_DENIED: "This agent's scope set does not allow this operation.",
10021
+ NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
10022
+ CONFLICT: "The Raft Server refused the operation because of the current state.",
10023
+ IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different message.",
10024
+ UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
10025
+ UNAVAILABLE: "The Raft Server could not serve this operation right now."
9940
10026
  };
9941
- function requireServerUrl(value) {
9942
- const serverUrl = value.trim();
9943
- if (!serverUrl) throw new RaftSdkConfigurationError("MISSING_SERVER_URL", "Raft Server URL is required");
9944
- let parsed;
9945
- try {
9946
- parsed = new URL(serverUrl);
9947
- } catch {
9948
- throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must be an absolute HTTP(S) URL");
10027
+ const DEFAULT_NEXT_ACTION = {
10028
+ INVALID_REQUEST: "Fix the request arguments; nothing was sent.",
10029
+ TRANSPORT_ERROR: "Check connectivity to the Raft Server and retry if the operation is safe to repeat.",
10030
+ HTTP_ERROR: "Read the Server's error code and adjust the request.",
10031
+ INVALID_RESPONSE: "Upgrade the SDK or report the Server version; the response shape is not the one this SDK knows.",
10032
+ CAPABILITY_NOT_AUTHORIZED: "Ask a human who can mint credentials to include the missing capability.",
10033
+ SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
10034
+ NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
10035
+ CONFLICT: "Read the current state before repeating this operation.",
10036
+ IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different message, or resend the identical payload to reconcile.",
10037
+ UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
10038
+ UNAVAILABLE: "Retry in a moment."
10039
+ };
10040
+ const RETRYABLE = {
10041
+ INVALID_REQUEST: false,
10042
+ TRANSPORT_ERROR: true,
10043
+ HTTP_ERROR: false,
10044
+ INVALID_RESPONSE: false,
10045
+ CAPABILITY_NOT_AUTHORIZED: false,
10046
+ SCOPE_DENIED: false,
10047
+ NOT_FOUND: false,
10048
+ CONFLICT: false,
10049
+ IDEMPOTENCY_KEY_REUSED: false,
10050
+ UNSUPPORTED_FOR_EXTERNAL_AGENTS: false,
10051
+ UNAVAILABLE: true
10052
+ };
10053
+ function classifyHttp(status, serverCode) {
10054
+ const code = serverCode?.toLowerCase() ?? "";
10055
+ if (code === "idempotency_key_reused") return "IDEMPOTENCY_KEY_REUSED";
10056
+ if (code === "reminders_unsupported_for_external_agents") return "UNSUPPORTED_FOR_EXTERNAL_AGENTS";
10057
+ if (code === "capability_not_authorized") return "CAPABILITY_NOT_AUTHORIZED";
10058
+ if (code === "scope_denied") return "SCOPE_DENIED";
10059
+ if (status === 404) return "NOT_FOUND";
10060
+ if (status === 409) return "CONFLICT";
10061
+ if (status === 503) return "UNAVAILABLE";
10062
+ if (status >= 500) return "UNAVAILABLE";
10063
+ return "HTTP_ERROR";
10064
+ }
10065
+ function opErrorFromClientError(error, status) {
10066
+ if (error.kind === "transport") return {
10067
+ code: "TRANSPORT_ERROR",
10068
+ message: DEFAULT_MESSAGES.TRANSPORT_ERROR,
10069
+ nextAction: DEFAULT_NEXT_ACTION.TRANSPORT_ERROR,
10070
+ retryable: true
10071
+ };
10072
+ if (error.kind === "http") {
10073
+ const serverCode = typeof error.errorCode === "string" && SERVER_ERROR_CODE.test(error.errorCode) ? error.errorCode : void 0;
10074
+ const code = classifyHttp(error.status, serverCode);
10075
+ const nextAction = typeof error.suggestedNextAction === "string" && error.suggestedNextAction.trim() ? error.suggestedNextAction.trim() : DEFAULT_NEXT_ACTION[code];
10076
+ return {
10077
+ code,
10078
+ message: DEFAULT_MESSAGES[code],
10079
+ status: error.status,
10080
+ ...serverCode ? { serverCode } : {},
10081
+ nextAction,
10082
+ retryable: RETRYABLE[code]
10083
+ };
9949
10084
  }
9950
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must use HTTP or HTTPS");
9951
- if (parsed.username || parsed.password) throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must not contain credentials");
9952
- return serverUrl.replace(/\/+$/, "");
10085
+ const code = error.reason === "request_contract_mismatch" || error.reason === "missing_path_param" || error.reason === "missing_route" ? "INVALID_REQUEST" : "INVALID_RESPONSE";
10086
+ return {
10087
+ code,
10088
+ message: DEFAULT_MESSAGES[code],
10089
+ ...status === void 0 ? {} : { status },
10090
+ nextAction: DEFAULT_NEXT_ACTION[code],
10091
+ retryable: false
10092
+ };
9953
10093
  }
9954
- function requireAgentCredential(value) {
9955
- const credential = value.trim();
9956
- if (!credential) throw new RaftSdkConfigurationError("MISSING_AGENT_CREDENTIAL", "Raft Agent credential is required");
9957
- if (!credential.startsWith("sk_agent_") || credential.length === 9) throw new RaftSdkConfigurationError("INVALID_AGENT_CREDENTIAL", "Raft Agent credential must be a long-lived External Agent credential");
9958
- return credential;
10094
+ function opError(code, overrides = {}) {
10095
+ return {
10096
+ code,
10097
+ message: DEFAULT_MESSAGES[code],
10098
+ nextAction: DEFAULT_NEXT_ACTION[code],
10099
+ retryable: RETRYABLE[code],
10100
+ ...overrides
10101
+ };
9959
10102
  }
9960
- /**
9961
- * Create a typed client for the credential-derived `/internal/agent-api/*`
9962
- * surface. The SDK does not read CLI profiles, environment variables, or the
9963
- * host user's home directory: the caller chooses and supplies the credential.
9964
- */
9965
- function createRaftClient(options) {
9966
- const serverUrl = requireServerUrl(options.serverUrl);
9967
- const auth = { authorization: `Bearer ${requireAgentCredential(options.credential)}` };
9968
- const messageClient = createAgentApiMessageClient({
9969
- baseUrl: serverUrl,
9970
- fetch: options.fetch,
9971
- headers: options.headers,
9972
- retry: options.retry,
9973
- throttle: options.throttle,
9974
- auth
9975
- });
9976
- const agentApi = createAgentApiClient({ fetch: {
9977
- baseUrl: serverUrl,
9978
- fetch: options.fetch,
9979
- headers: options.headers,
9980
- retry: { attempts: Math.min(Math.max(1, options.retry?.attempts ?? 1), 5) },
9981
- throttle: options.throttle,
9982
- auth
9983
- } });
9984
- const writeApi = createAgentApiClient({ fetch: {
9985
- baseUrl: serverUrl,
9986
- fetch: options.fetch,
9987
- headers: options.headers,
9988
- retry: { attempts: 1 },
9989
- throttle: options.throttle,
9990
- auth
9991
- } });
9992
- const eventsApi = createAgentApiClient({ fetch: {
9993
- baseUrl: serverUrl,
9994
- fetch: (input, init) => (options.fetch ?? fetch)(input, {
9995
- ...init,
9996
- cache: "no-store",
9997
- redirect: "error"
9998
- }),
9999
- headers: Object.fromEntries(new Headers(options.headers)),
10000
- retry: { attempts: 1 },
10001
- throttle: options.throttle,
10002
- auth
10003
- } });
10103
+ /** Render an error the way an agent should read it: what failed, the code, and the next action. */
10104
+ function formatOpErrorText(error) {
10105
+ const lines = [`Error: ${error.message}`, `Code: ${error.code}`];
10106
+ if (error.serverCode) lines.push(`Server code: ${error.serverCode}`);
10107
+ lines.push(`Retryable: ${error.retryable ? "yes" : "no"}`);
10108
+ lines.push(`Next action: ${error.nextAction}`);
10109
+ return lines.join("\n");
10110
+ }
10111
+ function failureOutcome(error) {
10004
10112
  return {
10005
- ...messageClient,
10006
- ...createRaftManageClient(agentApi, writeApi, {
10007
- serverUrl,
10008
- fetch: options.fetch ?? fetch,
10009
- headers: Object.fromEntries(new Headers(options.headers)),
10010
- authorization: auth.authorization,
10011
- beforeRequest: options.throttle?.beforeRequest
10012
- }),
10013
- events: { receive: (request) => receiveRaftEvents(eventsApi, request) },
10014
- channels: { join: (request) => joinRaftChannelByTarget(agentApi, request) },
10015
- routes: createRaftRoutes({
10016
- serverUrl,
10017
- fetch: options.fetch,
10018
- headers: options.headers,
10019
- authorization: auth.authorization,
10020
- readAttempts: options.retry?.attempts,
10021
- beforeRequest: options.throttle?.beforeRequest
10022
- }),
10023
- agent: { context: () => getRaftContext(agentApi) }
10113
+ ok: false,
10114
+ state: "error",
10115
+ error,
10116
+ next: {
10117
+ kind: "recover",
10118
+ why: error.nextAction
10119
+ },
10120
+ text: formatOpErrorText(error)
10024
10121
  };
10025
10122
  }
10123
+ /** Map a shared-client failure to a failure outcome. */
10124
+ function failureFromClientResult(result) {
10125
+ return failureOutcome(opErrorFromClientError(result.error, result.status));
10126
+ }
10026
10127
  //#endregion
10027
- //#region ../shared/src/clock.ts
10028
- function currentTimeMs() {
10029
- return Date.now();
10128
+ //#region ../shared/src/agentInbox.ts
10129
+ const NON_MEMBER_MENTION_REPLY_GUIDANCE = "[Raft notice: You were notified as a non-member, so you cannot reply in that channel. If no reply is needed, no action is required. Otherwise, DM the person who mentioned you or join the channel to participate.]";
10130
+ function projectAgentReplyAffordance(facts) {
10131
+ if (facts.non_member_mention !== true) return null;
10132
+ return {
10133
+ kind: "non_member_mention",
10134
+ guidance: NON_MEMBER_MENTION_REPLY_GUIDANCE
10135
+ };
10030
10136
  }
10031
- function currentDate() {
10032
- return new Date(currentTimeMs());
10137
+ function formatAgentReplyAffordance(facts) {
10138
+ return projectAgentReplyAffordance(facts)?.guidance ?? "";
10139
+ }
10140
+ function formatAgentReplyAffordanceSuffix(facts) {
10141
+ const guidance = formatAgentReplyAffordance(facts);
10142
+ return guidance ? `\n${guidance}` : "";
10033
10143
  }
10034
10144
  //#endregion
10035
- //#region src/credential.ts
10036
- var RaftCredentialError = class extends Error {
10037
- code;
10038
- name = "RaftCredentialError";
10039
- constructor(code, message) {
10040
- super(message);
10041
- this.code = code;
10042
- }
10043
- };
10044
- function isNonEmptyString(value) {
10045
- return typeof value === "string" && value.trim().length > 0;
10145
+ //#region ../shared/src/raftRefs.ts
10146
+ const RAFT_REF_CHANNEL_NAME_PATTERN = String.raw`[\p{L}\p{N}_-]+`;
10147
+ const RAFT_REF_USER_NAME_PATTERN = RAFT_REF_CHANNEL_NAME_PATTERN;
10148
+ const RAFT_REF_DM_PEER_PATTERN = String.raw`[\w.-]+(?:~(?:agent|human))?(?![\w.~-])`;
10149
+ const RAFT_REF_THREAD_SHORT_ID_PATTERN = String.raw`[\da-f]{6,8}`;
10150
+ const RAFT_REF_MESSAGE_ID_PATTERN = String.raw`[A-Za-z0-9][A-Za-z0-9-]{1,63}`;
10151
+ const RAFT_REF_TASK_NUMBER_PATTERN = String.raw`[1-9]\d*`;
10152
+ const RAFT_REF_COMPUTER_ID_PATTERN = String.raw`[\da-f]{8}-[\da-f]{4}-[1-5][\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}`;
10153
+ const RAFT_REF_APP_ID_PATTERN = String.raw`[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+`;
10154
+ function createRaftUserRefRegex() {
10155
+ return new RegExp(`(^|[^\\p{L}\\p{N}_-])@(${RAFT_REF_USER_NAME_PATTERN})`, "gu");
10046
10156
  }
10047
- function parseIdentityResponse(value) {
10048
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
10049
- const input = value;
10050
- if (!isNonEmptyString(input.agentId) || !isNonEmptyString(input.agentName) || input.agentDisplayName !== null && typeof input.agentDisplayName !== "string" || !isNonEmptyString(input.serverId) || !isNonEmptyString(input.credentialId) || !Array.isArray(input.scopes) || !input.scopes.every(isNonEmptyString)) return null;
10051
- return {
10052
- agentId: input.agentId,
10053
- agentName: input.agentName,
10054
- agentDisplayName: input.agentDisplayName,
10055
- serverId: input.serverId,
10056
- credentialId: input.credentialId,
10057
- scopes: [...input.scopes]
10058
- };
10157
+ function createRaftChannelThreadRefRegex() {
10158
+ return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})`, "giu");
10059
10159
  }
10060
- function parseStoredRaftCredential(value) {
10061
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
10062
- const input = value;
10063
- if (input.schemaVersion !== 1 || !isNonEmptyString(input.serverUrl) || !isNonEmptyString(input.credential) || !isNonEmptyString(input.storedAt)) return null;
10064
- const identity = parseIdentityResponse(input);
10065
- if (!identity || Number.isNaN(Date.parse(input.storedAt))) return null;
10066
- let serverUrl;
10067
- let credential;
10068
- try {
10069
- serverUrl = requireServerUrl(input.serverUrl);
10070
- credential = requireAgentCredential(input.credential);
10071
- } catch {
10072
- return null;
10073
- }
10074
- return {
10075
- schemaVersion: 1,
10076
- serverUrl,
10077
- credential,
10078
- storedAt: input.storedAt,
10079
- ...identity
10080
- };
10160
+ function createRaftChannelRefRegex() {
10161
+ return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN})`, "gu");
10162
+ }
10163
+ function createRaftDmThreadRefRegex() {
10164
+ return new RegExp(`dm:@(${RAFT_REF_DM_PEER_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})`, "giu");
10165
+ }
10166
+ function createRaftDmRefRegex() {
10167
+ return new RegExp(`dm:@(${RAFT_REF_DM_PEER_PATTERN})`, "giu");
10081
10168
  }
10082
10169
  /**
10083
- * Validate an existing External Agent credential against Raft, derive its
10084
- * bound identity from the credential-authenticated `whoami` surface, and save
10085
- * it through the caller's explicit store. This does not mint or rotate a
10086
- * credential and never returns the credential bytes.
10170
+ * A `dm:@name~<suffix>` token whose suffix is not a known peer kind
10171
+ * (`~bot`, `~agentXYZ`, a bare `~`). Renderers claim it as plain text so it
10172
+ * never degrades into a link to a different target (`dm:@name` or `@name`).
10173
+ * Run after the valid DM passes; it also matches valid refs.
10087
10174
  */
10088
- async function bootstrapRaftCredential(options) {
10089
- const serverUrl = requireServerUrl(options.serverUrl);
10090
- const credential = requireAgentCredential(options.credential);
10091
- const httpFetch = options.fetch ?? globalThis.fetch;
10092
- let existing;
10093
- try {
10094
- existing = await options.store.load();
10095
- } catch {
10096
- throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential could not be checked before saving");
10097
- }
10098
- if (existing) {
10099
- const validatedExisting = parseStoredRaftCredential(existing);
10100
- if (!validatedExisting) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential record is invalid");
10101
- existing = validatedExisting;
10102
- if (existing.serverUrl !== serverUrl || existing.credential !== credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
10103
- }
10104
- let response;
10105
- try {
10106
- response = await httpFetch(`${serverUrl}/internal/agent-api/`, {
10107
- method: "GET",
10108
- headers: { authorization: `Bearer ${credential}` }
10109
- });
10110
- } catch {
10111
- throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", "Could not reach the Raft Server to verify the Agent credential");
10112
- }
10113
- if (!response.ok) {
10114
- if (response.status === 401 || response.status === 403) throw new RaftCredentialError("CREDENTIAL_REJECTED", "The Raft Server rejected the Agent credential");
10115
- throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", `The Raft Server could not verify the Agent credential (status ${response.status})`);
10116
- }
10117
- let payload;
10118
- try {
10119
- payload = await response.json();
10120
- } catch {
10121
- throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
10122
- }
10123
- const identity = parseIdentityResponse(payload);
10124
- if (!identity) throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
10125
- if (!identity.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The Raft Agent credential does not include the send capability required by this SDK");
10126
- if (existing && (existing.serverId !== identity.serverId || existing.agentId !== identity.agentId)) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store is already bound to a different Raft Agent identity");
10127
- if (existing) return {
10128
- serverUrl,
10129
- ...identity
10175
+ function createRaftMalformedDmKindRefRegex() {
10176
+ return new RegExp(String.raw`dm:@[\w.-]+~[\w.~-]*(?::${RAFT_REF_THREAD_SHORT_ID_PATTERN})?`, "giu");
10177
+ }
10178
+ function createRaftMessageRefRegex() {
10179
+ return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})`, "giu");
10180
+ }
10181
+ function createRaftBareTaskRefRegex() {
10182
+ return new RegExp(`(^|[^\\w/])(?:(task\\s+))?#(${RAFT_REF_TASK_NUMBER_PATTERN})\\b`, "giu");
10183
+ }
10184
+ const USER_RE = new RegExp(String.raw`^@(${RAFT_REF_USER_NAME_PATTERN})$`, "u");
10185
+ const COMPUTER_RE = new RegExp(String.raw`^computer:(${RAFT_REF_COMPUTER_ID_PATTERN})$`, "iu");
10186
+ const APP_RE = new RegExp(String.raw`^app:(${RAFT_REF_APP_ID_PATTERN})$`, "u");
10187
+ const CHANNEL_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN})$`, "u");
10188
+ const CHANNEL_THREAD_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})$`, "iu");
10189
+ const DM_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN})$`, "iu");
10190
+ const DM_THREAD_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})$`, "iu");
10191
+ const TASK_RE = new RegExp(String.raw`^task\s+#(${RAFT_REF_TASK_NUMBER_PATTERN})$`, "iu");
10192
+ const CHANNEL_MESSAGE_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})$`, "iu");
10193
+ const DM_MESSAGE_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})$`, "iu");
10194
+ function parseRaftRefTarget(rawTarget) {
10195
+ const target = rawTarget.trim();
10196
+ if (!target) return null;
10197
+ const channelMessage = CHANNEL_MESSAGE_RE.exec(target);
10198
+ if (channelMessage) return {
10199
+ kind: "message",
10200
+ channelName: channelMessage[1],
10201
+ threadParentShortId: channelMessage[2] ?? null,
10202
+ messageId: channelMessage[3]
10130
10203
  };
10131
- const record = {
10132
- schemaVersion: 1,
10133
- serverUrl,
10134
- credential,
10135
- storedAt: currentDate().toISOString(),
10136
- ...identity
10204
+ const dmMessage = DM_MESSAGE_RE.exec(target);
10205
+ if (dmMessage) return {
10206
+ kind: "dm-message",
10207
+ peerName: dmMessage[1],
10208
+ threadParentShortId: dmMessage[2] ?? null,
10209
+ messageId: dmMessage[3]
10137
10210
  };
10138
- try {
10139
- await options.store.save(record);
10140
- } catch (error) {
10141
- if (error instanceof RaftCredentialError && error.code === "CREDENTIAL_STORE_CONFLICT") throw error;
10142
- throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The verified Agent credential could not be saved");
10143
- }
10144
- return {
10145
- serverUrl,
10146
- ...identity
10211
+ const channelThread = CHANNEL_THREAD_RE.exec(target);
10212
+ if (channelThread) return {
10213
+ kind: "channel-thread",
10214
+ channelName: channelThread[1],
10215
+ threadShortId: channelThread[2]
10216
+ };
10217
+ const dmThread = DM_THREAD_RE.exec(target);
10218
+ if (dmThread) return {
10219
+ kind: "dm-thread",
10220
+ peerName: dmThread[1],
10221
+ threadShortId: dmThread[2]
10222
+ };
10223
+ const task = TASK_RE.exec(target);
10224
+ if (task) return {
10225
+ kind: "task",
10226
+ taskNumber: Number(task[1])
10227
+ };
10228
+ const computer = COMPUTER_RE.exec(target);
10229
+ if (computer) return {
10230
+ kind: "computer",
10231
+ machineId: computer[1].toLowerCase()
10232
+ };
10233
+ const app = APP_RE.exec(target);
10234
+ if (app) return {
10235
+ kind: "app",
10236
+ appId: app[1]
10237
+ };
10238
+ const user = USER_RE.exec(target);
10239
+ if (user) return {
10240
+ kind: "user",
10241
+ name: user[1]
10242
+ };
10243
+ const channel = CHANNEL_RE.exec(target);
10244
+ if (channel) return {
10245
+ kind: "channel",
10246
+ channelName: channel[1]
10247
+ };
10248
+ const dm = DM_RE.exec(target);
10249
+ if (dm) return {
10250
+ kind: "dm",
10251
+ peerName: dm[1]
10147
10252
  };
10253
+ return null;
10148
10254
  }
10149
- /** Load the caller-selected credential store and create a normal Raft client. */
10150
- async function createRaftClientFromStore(options) {
10151
- let record;
10152
- try {
10153
- record = await options.store.load();
10154
- } catch {
10155
- throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential could not be loaded");
10255
+ function formatRaftRefTarget(target) {
10256
+ switch (target.kind) {
10257
+ case "user": return `@${target.name}`;
10258
+ case "computer": return `computer:${target.machineId}`;
10259
+ case "app": return `app:${target.appId}`;
10260
+ case "channel": return `#${target.channelName}`;
10261
+ case "channel-thread": return `#${target.channelName}:${target.threadShortId}`;
10262
+ case "dm": return `dm:@${target.peerName}`;
10263
+ case "dm-thread": return `dm:@${target.peerName}:${target.threadShortId}`;
10264
+ case "task": return `task #${target.taskNumber}`;
10265
+ case "message": return target.threadParentShortId ? `#${target.channelName}:${target.threadParentShortId} msg=${target.messageId}` : `#${target.channelName} msg=${target.messageId}`;
10266
+ case "dm-message": return target.threadParentShortId ? `dm:@${target.peerName}:${target.threadParentShortId} msg=${target.messageId}` : `dm:@${target.peerName} msg=${target.messageId}`;
10156
10267
  }
10157
- if (!record) throw new RaftCredentialError("CREDENTIAL_NOT_FOUND", "No stored Raft Agent credential was found");
10158
- const validated = parseStoredRaftCredential(record);
10159
- if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential is invalid");
10160
- if (!validated.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The stored Raft Agent credential does not include the send capability required by this SDK");
10161
- return createRaftClient({
10162
- serverUrl: validated.serverUrl,
10163
- credential: validated.credential,
10164
- fetch: options.fetch,
10165
- headers: options.headers,
10166
- retry: options.retry,
10167
- throttle: options.throttle
10268
+ }
10269
+ function markdownCodeSpans(source) {
10270
+ const spans = [];
10271
+ for (const match of source.matchAll(/```[\s\S]*?```/g)) spans.push({
10272
+ start: match.index ?? 0,
10273
+ end: (match.index ?? 0) + match[0].length
10168
10274
  });
10169
- }
10170
- //#endregion
10171
- //#region src/fileCredentialStore.ts
10172
- const MAX_CREDENTIAL_FILE_BYTES = 65536;
10173
- /**
10174
- * Create a Node.js credential store at one explicit absolute path. The SDK
10175
- * never discovers CLI profiles or a home directory. Writes use a same-directory
10176
- * temporary file followed by atomic rename, and the final file is mode 0600.
10177
- */
10178
- function createFileCredentialStore(filePath) {
10179
- if (!filePath.trim() || !(0, node_path.isAbsolute)(filePath)) throw new RaftSdkConfigurationError("INVALID_CREDENTIAL_PATH", "Raft credential file path must be an explicit absolute path");
10180
- return {
10181
- load: () => loadFileCredential(filePath),
10182
- async save(record) {
10183
- const validated = parseStoredRaftCredential(record);
10184
- if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "Refusing to write an invalid Raft Agent credential record");
10185
- const directory = (0, node_path.dirname)(filePath);
10186
- const temporaryPath = `${filePath}.${process.pid}.${(0, node_crypto.randomUUID)()}.tmp`;
10187
- let handle;
10188
- try {
10189
- await (0, node_fs_promises.mkdir)(directory, {
10190
- recursive: true,
10191
- mode: 448
10192
- });
10193
- const existing = await loadFileCredential(filePath);
10194
- if (existing) {
10195
- assertSameCredential(existing, validated);
10196
- return;
10197
- }
10198
- handle = await (0, node_fs_promises.open)(temporaryPath, "wx", 384);
10199
- await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`, "utf8");
10200
- await handle.sync();
10201
- await (0, node_fs_promises.chmod)(temporaryPath, 384);
10202
- await handle.close();
10203
- handle = void 0;
10204
- try {
10205
- await (0, node_fs_promises.link)(temporaryPath, filePath);
10206
- } catch (error) {
10207
- if (error.code !== "EEXIST") throw error;
10208
- const winner = await loadFileCredential(filePath);
10209
- if (!winner) throw error;
10210
- assertSameCredential(winner, validated);
10211
- }
10212
- await (0, node_fs_promises.unlink)(temporaryPath);
10213
- } catch (error) {
10214
- if (handle) await handle.close().catch(() => void 0);
10215
- await (0, node_fs_promises.unlink)(temporaryPath).catch(() => void 0);
10216
- if (error instanceof RaftCredentialError) throw error;
10217
- throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The Raft Agent credential could not be written atomically");
10218
- }
10219
- }
10220
- };
10221
- }
10222
- async function loadFileCredential(filePath) {
10223
- let initialMetadata;
10224
- try {
10225
- initialMetadata = await (0, node_fs_promises.lstat)(filePath);
10226
- } catch (error) {
10227
- if (error.code === "ENOENT") return null;
10228
- throw fileReadError();
10229
- }
10230
- if (!initialMetadata.isFile() || initialMetadata.isSymbolicLink() || initialMetadata.size > MAX_CREDENTIAL_FILE_BYTES) throw fileReadError();
10231
- let parsed;
10232
- let handle;
10233
- try {
10234
- const noFollow = process.platform === "win32" ? 0 : node_fs.constants.O_NOFOLLOW;
10235
- handle = await (0, node_fs_promises.open)(filePath, node_fs.constants.O_RDONLY | noFollow);
10236
- const openedMetadata = await handle.stat();
10237
- if (!openedMetadata.isFile() || openedMetadata.size > MAX_CREDENTIAL_FILE_BYTES || initialMetadata.dev !== openedMetadata.dev || initialMetadata.ino !== openedMetadata.ino) throw fileReadError();
10238
- if (process.platform !== "win32" && (openedMetadata.mode & 63) !== 0) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file permissions are too broad; expected mode 0600");
10239
- parsed = JSON.parse(await handle.readFile("utf8"));
10240
- } catch (error) {
10241
- if (error instanceof RaftCredentialError) throw error;
10242
- throw fileReadError();
10243
- } finally {
10244
- await handle?.close().catch(() => void 0);
10245
- }
10246
- const record = parseStoredRaftCredential(parsed);
10247
- if (!record) throw fileReadError();
10248
- return record;
10249
- }
10250
- function assertSameCredential(existing, candidate) {
10251
- if (existing.credential !== candidate.credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
10252
- }
10253
- function fileReadError() {
10254
- return new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file is invalid or unreadable");
10255
- }
10256
- //#endregion
10257
- //#region ../shared/src/agentOps/outcome.ts
10258
- const SERVER_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
10259
- const DEFAULT_MESSAGES = {
10260
- INVALID_REQUEST: "The request did not match the Agent API contract; nothing was sent.",
10261
- TRANSPORT_ERROR: "The request did not reach the Raft Server.",
10262
- HTTP_ERROR: "The Raft Server rejected the request.",
10263
- INVALID_RESPONSE: "The Raft Server response did not match the Agent API contract.",
10264
- CAPABILITY_NOT_AUTHORIZED: "This credential does not carry the capability this operation needs.",
10265
- SCOPE_DENIED: "This agent's scope set does not allow this operation.",
10266
- NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
10267
- CONFLICT: "The Raft Server refused the operation because of the current state.",
10268
- IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different message.",
10269
- UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
10270
- UNAVAILABLE: "The Raft Server could not serve this operation right now."
10271
- };
10272
- const DEFAULT_NEXT_ACTION = {
10273
- INVALID_REQUEST: "Fix the request arguments; nothing was sent.",
10274
- TRANSPORT_ERROR: "Check connectivity to the Raft Server and retry if the operation is safe to repeat.",
10275
- HTTP_ERROR: "Read the Server's error code and adjust the request.",
10276
- INVALID_RESPONSE: "Upgrade the SDK or report the Server version; the response shape is not the one this SDK knows.",
10277
- CAPABILITY_NOT_AUTHORIZED: "Ask a human who can mint credentials to include the missing capability.",
10278
- SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
10279
- NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
10280
- CONFLICT: "Read the current state before repeating this operation.",
10281
- IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different message, or resend the identical payload to reconcile.",
10282
- UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
10283
- UNAVAILABLE: "Retry in a moment."
10284
- };
10285
- const RETRYABLE = {
10286
- INVALID_REQUEST: false,
10287
- TRANSPORT_ERROR: true,
10288
- HTTP_ERROR: false,
10289
- INVALID_RESPONSE: false,
10290
- CAPABILITY_NOT_AUTHORIZED: false,
10291
- SCOPE_DENIED: false,
10292
- NOT_FOUND: false,
10293
- CONFLICT: false,
10294
- IDEMPOTENCY_KEY_REUSED: false,
10295
- UNSUPPORTED_FOR_EXTERNAL_AGENTS: false,
10296
- UNAVAILABLE: true
10297
- };
10298
- function classifyHttp(status, serverCode) {
10299
- const code = serverCode?.toLowerCase() ?? "";
10300
- if (code === "idempotency_key_reused") return "IDEMPOTENCY_KEY_REUSED";
10301
- if (code === "reminders_unsupported_for_external_agents") return "UNSUPPORTED_FOR_EXTERNAL_AGENTS";
10302
- if (code === "capability_not_authorized") return "CAPABILITY_NOT_AUTHORIZED";
10303
- if (code === "scope_denied") return "SCOPE_DENIED";
10304
- if (status === 404) return "NOT_FOUND";
10305
- if (status === 409) return "CONFLICT";
10306
- if (status === 503) return "UNAVAILABLE";
10307
- if (status >= 500) return "UNAVAILABLE";
10308
- return "HTTP_ERROR";
10309
- }
10310
- function opErrorFromClientError(error, status) {
10311
- if (error.kind === "transport") return {
10312
- code: "TRANSPORT_ERROR",
10313
- message: DEFAULT_MESSAGES.TRANSPORT_ERROR,
10314
- nextAction: DEFAULT_NEXT_ACTION.TRANSPORT_ERROR,
10315
- retryable: true
10316
- };
10317
- if (error.kind === "http") {
10318
- const serverCode = typeof error.errorCode === "string" && SERVER_ERROR_CODE.test(error.errorCode) ? error.errorCode : void 0;
10319
- const code = classifyHttp(error.status, serverCode);
10320
- const nextAction = typeof error.suggestedNextAction === "string" && error.suggestedNextAction.trim() ? error.suggestedNextAction.trim() : DEFAULT_NEXT_ACTION[code];
10321
- return {
10322
- code,
10323
- message: DEFAULT_MESSAGES[code],
10324
- status: error.status,
10325
- ...serverCode ? { serverCode } : {},
10326
- nextAction,
10327
- retryable: RETRYABLE[code]
10328
- };
10329
- }
10330
- const code = error.reason === "request_contract_mismatch" || error.reason === "missing_path_param" || error.reason === "missing_route" ? "INVALID_REQUEST" : "INVALID_RESPONSE";
10331
- return {
10332
- code,
10333
- message: DEFAULT_MESSAGES[code],
10334
- ...status === void 0 ? {} : { status },
10335
- nextAction: DEFAULT_NEXT_ACTION[code],
10336
- retryable: false
10337
- };
10338
- }
10339
- function opError(code, overrides = {}) {
10340
- return {
10341
- code,
10342
- message: DEFAULT_MESSAGES[code],
10343
- nextAction: DEFAULT_NEXT_ACTION[code],
10344
- retryable: RETRYABLE[code],
10345
- ...overrides
10346
- };
10347
- }
10348
- /** Render an error the way an agent should read it: what failed, the code, and the next action. */
10349
- function formatOpErrorText(error) {
10350
- const lines = [`Error: ${error.message}`, `Code: ${error.code}`];
10351
- if (error.serverCode) lines.push(`Server code: ${error.serverCode}`);
10352
- lines.push(`Retryable: ${error.retryable ? "yes" : "no"}`);
10353
- lines.push(`Next action: ${error.nextAction}`);
10354
- return lines.join("\n");
10355
- }
10356
- function failureOutcome(error) {
10357
- return {
10358
- ok: false,
10359
- state: "error",
10360
- error,
10361
- next: {
10362
- kind: "recover",
10363
- why: error.nextAction
10364
- },
10365
- text: formatOpErrorText(error)
10366
- };
10367
- }
10368
- /** Map a shared-client failure to a failure outcome. */
10369
- function failureFromClientResult(result) {
10370
- return failureOutcome(opErrorFromClientError(result.error, result.status));
10371
- }
10372
- //#endregion
10373
- //#region ../shared/src/agentInbox.ts
10374
- const NON_MEMBER_MENTION_REPLY_GUIDANCE = "[Raft notice: You were notified as a non-member, so you cannot reply in that channel. If no reply is needed, no action is required. Otherwise, DM the person who mentioned you or join the channel to participate.]";
10375
- function projectAgentReplyAffordance(facts) {
10376
- if (facts.non_member_mention !== true) return null;
10377
- return {
10378
- kind: "non_member_mention",
10379
- guidance: NON_MEMBER_MENTION_REPLY_GUIDANCE
10380
- };
10381
- }
10382
- function formatAgentReplyAffordance(facts) {
10383
- return projectAgentReplyAffordance(facts)?.guidance ?? "";
10384
- }
10385
- function formatAgentReplyAffordanceSuffix(facts) {
10386
- const guidance = formatAgentReplyAffordance(facts);
10387
- return guidance ? `\n${guidance}` : "";
10388
- }
10389
- //#endregion
10390
- //#region ../shared/src/raftRefs.ts
10391
- const RAFT_REF_CHANNEL_NAME_PATTERN = String.raw`[\p{L}\p{N}_-]+`;
10392
- const RAFT_REF_USER_NAME_PATTERN = RAFT_REF_CHANNEL_NAME_PATTERN;
10393
- const RAFT_REF_DM_PEER_PATTERN = String.raw`[\w.-]+(?:~(?:agent|human))?(?![\w.~-])`;
10394
- const RAFT_REF_THREAD_SHORT_ID_PATTERN = String.raw`[\da-f]{6,8}`;
10395
- const RAFT_REF_MESSAGE_ID_PATTERN = String.raw`[A-Za-z0-9][A-Za-z0-9-]{1,63}`;
10396
- const RAFT_REF_TASK_NUMBER_PATTERN = String.raw`[1-9]\d*`;
10397
- const RAFT_REF_COMPUTER_ID_PATTERN = String.raw`[\da-f]{8}-[\da-f]{4}-[1-5][\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}`;
10398
- const RAFT_REF_APP_ID_PATTERN = String.raw`[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+`;
10399
- function createRaftUserRefRegex() {
10400
- return new RegExp(`(^|[^\\p{L}\\p{N}_-])@(${RAFT_REF_USER_NAME_PATTERN})`, "gu");
10401
- }
10402
- function createRaftChannelThreadRefRegex() {
10403
- return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})`, "giu");
10404
- }
10405
- function createRaftChannelRefRegex() {
10406
- return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN})`, "gu");
10407
- }
10408
- function createRaftDmThreadRefRegex() {
10409
- return new RegExp(`dm:@(${RAFT_REF_DM_PEER_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})`, "giu");
10410
- }
10411
- function createRaftDmRefRegex() {
10412
- return new RegExp(`dm:@(${RAFT_REF_DM_PEER_PATTERN})`, "giu");
10413
- }
10414
- /**
10415
- * A `dm:@name~<suffix>` token whose suffix is not a known peer kind
10416
- * (`~bot`, `~agentXYZ`, a bare `~`). Renderers claim it as plain text so it
10417
- * never degrades into a link to a different target (`dm:@name` or `@name`).
10418
- * Run after the valid DM passes; it also matches valid refs.
10419
- */
10420
- function createRaftMalformedDmKindRefRegex() {
10421
- return new RegExp(String.raw`dm:@[\w.-]+~[\w.~-]*(?::${RAFT_REF_THREAD_SHORT_ID_PATTERN})?`, "giu");
10422
- }
10423
- function createRaftMessageRefRegex() {
10424
- return new RegExp(`#(${RAFT_REF_CHANNEL_NAME_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})`, "giu");
10425
- }
10426
- function createRaftBareTaskRefRegex() {
10427
- return new RegExp(`(^|[^\\w/])(?:(task\\s+))?#(${RAFT_REF_TASK_NUMBER_PATTERN})\\b`, "giu");
10428
- }
10429
- const USER_RE = new RegExp(String.raw`^@(${RAFT_REF_USER_NAME_PATTERN})$`, "u");
10430
- const COMPUTER_RE = new RegExp(String.raw`^computer:(${RAFT_REF_COMPUTER_ID_PATTERN})$`, "iu");
10431
- const APP_RE = new RegExp(String.raw`^app:(${RAFT_REF_APP_ID_PATTERN})$`, "u");
10432
- const CHANNEL_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN})$`, "u");
10433
- const CHANNEL_THREAD_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})$`, "iu");
10434
- const DM_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN})$`, "iu");
10435
- const DM_THREAD_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN}):(${RAFT_REF_THREAD_SHORT_ID_PATTERN})$`, "iu");
10436
- const TASK_RE = new RegExp(String.raw`^task\s+#(${RAFT_REF_TASK_NUMBER_PATTERN})$`, "iu");
10437
- const CHANNEL_MESSAGE_RE = new RegExp(String.raw`^#(${RAFT_REF_CHANNEL_NAME_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})$`, "iu");
10438
- const DM_MESSAGE_RE = new RegExp(String.raw`^dm:@(${RAFT_REF_DM_PEER_PATTERN})(?::(${RAFT_REF_THREAD_SHORT_ID_PATTERN}))?\s+msg=(${RAFT_REF_MESSAGE_ID_PATTERN})$`, "iu");
10439
- function parseRaftRefTarget(rawTarget) {
10440
- const target = rawTarget.trim();
10441
- if (!target) return null;
10442
- const channelMessage = CHANNEL_MESSAGE_RE.exec(target);
10443
- if (channelMessage) return {
10444
- kind: "message",
10445
- channelName: channelMessage[1],
10446
- threadParentShortId: channelMessage[2] ?? null,
10447
- messageId: channelMessage[3]
10448
- };
10449
- const dmMessage = DM_MESSAGE_RE.exec(target);
10450
- if (dmMessage) return {
10451
- kind: "dm-message",
10452
- peerName: dmMessage[1],
10453
- threadParentShortId: dmMessage[2] ?? null,
10454
- messageId: dmMessage[3]
10455
- };
10456
- const channelThread = CHANNEL_THREAD_RE.exec(target);
10457
- if (channelThread) return {
10458
- kind: "channel-thread",
10459
- channelName: channelThread[1],
10460
- threadShortId: channelThread[2]
10461
- };
10462
- const dmThread = DM_THREAD_RE.exec(target);
10463
- if (dmThread) return {
10464
- kind: "dm-thread",
10465
- peerName: dmThread[1],
10466
- threadShortId: dmThread[2]
10467
- };
10468
- const task = TASK_RE.exec(target);
10469
- if (task) return {
10470
- kind: "task",
10471
- taskNumber: Number(task[1])
10472
- };
10473
- const computer = COMPUTER_RE.exec(target);
10474
- if (computer) return {
10475
- kind: "computer",
10476
- machineId: computer[1].toLowerCase()
10477
- };
10478
- const app = APP_RE.exec(target);
10479
- if (app) return {
10480
- kind: "app",
10481
- appId: app[1]
10482
- };
10483
- const user = USER_RE.exec(target);
10484
- if (user) return {
10485
- kind: "user",
10486
- name: user[1]
10487
- };
10488
- const channel = CHANNEL_RE.exec(target);
10489
- if (channel) return {
10490
- kind: "channel",
10491
- channelName: channel[1]
10492
- };
10493
- const dm = DM_RE.exec(target);
10494
- if (dm) return {
10495
- kind: "dm",
10496
- peerName: dm[1]
10497
- };
10498
- return null;
10499
- }
10500
- function formatRaftRefTarget(target) {
10501
- switch (target.kind) {
10502
- case "user": return `@${target.name}`;
10503
- case "computer": return `computer:${target.machineId}`;
10504
- case "app": return `app:${target.appId}`;
10505
- case "channel": return `#${target.channelName}`;
10506
- case "channel-thread": return `#${target.channelName}:${target.threadShortId}`;
10507
- case "dm": return `dm:@${target.peerName}`;
10508
- case "dm-thread": return `dm:@${target.peerName}:${target.threadShortId}`;
10509
- case "task": return `task #${target.taskNumber}`;
10510
- case "message": return target.threadParentShortId ? `#${target.channelName}:${target.threadParentShortId} msg=${target.messageId}` : `#${target.channelName} msg=${target.messageId}`;
10511
- case "dm-message": return target.threadParentShortId ? `dm:@${target.peerName}:${target.threadParentShortId} msg=${target.messageId}` : `dm:@${target.peerName} msg=${target.messageId}`;
10512
- }
10513
- }
10514
- function markdownCodeSpans(source) {
10515
- const spans = [];
10516
- for (const match of source.matchAll(/```[\s\S]*?```/g)) spans.push({
10517
- start: match.index ?? 0,
10518
- end: (match.index ?? 0) + match[0].length
10519
- });
10520
- const fences = [...spans];
10521
- const scanInline = (from, to) => {
10522
- if (from >= to) return;
10523
- const segment = source.slice(from, to);
10524
- for (const match of segment.matchAll(/``[^`]+``|`[^`]+`/g)) {
10525
- const start = from + (match.index ?? 0);
10526
- spans.push({
10527
- start,
10528
- end: start + match[0].length
10529
- });
10530
- }
10531
- };
10532
- let cursor = 0;
10533
- for (const fence of fences) {
10534
- scanInline(cursor, fence.start);
10535
- cursor = fence.end;
10536
- }
10537
- scanInline(cursor, source.length);
10538
- return spans.sort((a, b) => a.start - b.start);
10275
+ const fences = [...spans];
10276
+ const scanInline = (from, to) => {
10277
+ if (from >= to) return;
10278
+ const segment = source.slice(from, to);
10279
+ for (const match of segment.matchAll(/``[^`]+``|`[^`]+`/g)) {
10280
+ const start = from + (match.index ?? 0);
10281
+ spans.push({
10282
+ start,
10283
+ end: start + match[0].length
10284
+ });
10285
+ }
10286
+ };
10287
+ let cursor = 0;
10288
+ for (const fence of fences) {
10289
+ scanInline(cursor, fence.start);
10290
+ cursor = fence.end;
10291
+ }
10292
+ scanInline(cursor, source.length);
10293
+ return spans.sort((a, b) => a.start - b.start);
10539
10294
  }
10540
10295
  function visitMarkdownProseChunks(source, visitor) {
10541
10296
  const spans = markdownCodeSpans(source);
@@ -10966,34 +10721,9 @@ function unreadMessagesInterrupt(input) {
10966
10721
  contextComplete: !withheld && heldMessages.length + omittedMessageCount >= newMessageCount
10967
10722
  };
10968
10723
  }
10969
- /** `raft message send --send-draft` for the draft a held send saved, under its original key. */
10970
- function sendDraftResume(target, idempotencyKey, extraArgv = []) {
10971
- return {
10972
- argv: [
10973
- "message",
10974
- "send",
10975
- "--send-draft",
10976
- "--target",
10977
- target,
10978
- "--expected-draft-key",
10979
- idempotencyKey,
10980
- ...extraArgv
10981
- ],
10982
- idempotencyKey
10983
- };
10984
- }
10985
- /** `raft message send --discard-draft` for the draft a held send saved (compare-and-clear on its key). */
10986
- function discardDraftCancel(target, idempotencyKey, extraArgv = []) {
10987
- return { argv: [
10988
- "message",
10989
- "send",
10990
- "--discard-draft",
10991
- "--target",
10992
- target,
10993
- "--expected-draft-key",
10994
- idempotencyKey,
10995
- ...extraArgv
10996
- ] };
10724
+ /** Resume of an in-process SDK send: no argv (the SDK stores no draft), only the original key to call again with. */
10725
+ function inProcessSendResume(idempotencyKey) {
10726
+ return { idempotencyKey };
10997
10727
  }
10998
10728
  /** `raft task claim` argv for a claim request (resume of a held claim). */
10999
10729
  function taskClaimArgv(target, taskNumbers, messageIds, extraArgv = []) {
@@ -11555,7 +11285,7 @@ async function sendMessage(client, request, frontier) {
11555
11285
  if (!result.ok) return failureFromClientResult(result);
11556
11286
  const data = result.data;
11557
11287
  if (isHeldResponse(data)) {
11558
- const interrupt = heldInterrupt(request.target, data, "Your message was not sent.", sendDraftResume(request.target, idempotencyKey), discardDraftCancel(request.target, idempotencyKey));
11288
+ const interrupt = heldInterrupt(request.target, data, "Your message was not sent.", inProcessSendResume(idempotencyKey));
11559
11289
  return {
11560
11290
  ok: true,
11561
11291
  state: "interrupted",
@@ -13484,532 +13214,872 @@ async function resolveMessage(client, request) {
13484
13214
  text: formatAgentMessages([toAgentMessageLike(message.raw)])
13485
13215
  };
13486
13216
  }
13487
- async function reactToMessage(client, request, action = "add") {
13488
- const emoji = request.emoji?.trim();
13489
- if (!request.messageId?.trim() || !emoji || /\s/.test(emoji) || emoji.length > 16) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id and a single reaction emoji are required." }));
13490
- const params = { msgId: request.messageId };
13491
- const result = action === "add" ? await client.messages.addReaction(params, { emoji }) : await client.messages.removeReaction(params, { emoji });
13217
+ async function reactToMessage(client, request, action = "add") {
13218
+ const emoji = request.emoji?.trim();
13219
+ if (!request.messageId?.trim() || !emoji || /\s/.test(emoji) || emoji.length > 16) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id and a single reaction emoji are required." }));
13220
+ const params = { msgId: request.messageId };
13221
+ const result = action === "add" ? await client.messages.addReaction(params, { emoji }) : await client.messages.removeReaction(params, { emoji });
13222
+ if (!result.ok) return failureFromClientResult(result);
13223
+ const verb = action === "add" ? "added to" : "removed from";
13224
+ return {
13225
+ ok: true,
13226
+ state: action === "add" ? "added" : "removed",
13227
+ data: {
13228
+ messageId: request.messageId,
13229
+ emoji
13230
+ },
13231
+ next: null,
13232
+ text: `Reaction ${emoji} ${verb} message ${request.messageId.slice(0, 8)}.`
13233
+ };
13234
+ }
13235
+ //#endregion
13236
+ //#region ../shared/src/agentText/mentions.ts
13237
+ function normalizeAgentMentionAction(action) {
13238
+ if (action === "notify" || action === "notify_only") return "notify";
13239
+ if (action === "add" || action === "invite") return "add";
13240
+ return null;
13241
+ }
13242
+ function formatActionCommands(action) {
13243
+ const verbs = action.availableActions.map(normalizeAgentMentionAction).filter((verb) => verb !== null);
13244
+ return Array.from(new Set(verbs)).map((verb) => ` ${verb}: raft mention ${verb} ${action.resolutionId}`);
13245
+ }
13246
+ function formatAuthoredMentionToken(targetHandle) {
13247
+ return targetHandle.startsWith("@") ? targetHandle : `@${targetHandle}`;
13248
+ }
13249
+ const PENDING_MENTION_ACTION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13250
+ /** Per-token mention recovery command line; null when the id is not a UUID (fail closed). */
13251
+ function formatAgentMentionNotifyRecoveryCommand(resolutionId) {
13252
+ return PENDING_MENTION_ACTION_ID_RE.test(resolutionId) ? `raft mention notify ${resolutionId}` : null;
13253
+ }
13254
+ function toAgentSenderPendingMentionAction(action) {
13255
+ return {
13256
+ resolutionId: action.resolutionId,
13257
+ messageId: action.messageId,
13258
+ targetHandle: formatAuthoredMentionToken(action.targetHandle),
13259
+ status: "not_queued",
13260
+ reason: "not_in_conversation",
13261
+ consequence: "This @mention did not notify anyone.",
13262
+ expiresAt: action.expiresAt ?? null,
13263
+ recoveryCommand: formatAgentMentionNotifyRecoveryCommand(action.resolutionId)
13264
+ };
13265
+ }
13266
+ function toAgentSenderUnresolvedMentionWarning(targetHandle) {
13267
+ return {
13268
+ targetHandle: formatAuthoredMentionToken(targetHandle),
13269
+ status: "not_queued",
13270
+ reason: "unknown_or_not_visible",
13271
+ consequence: "This @mention did not notify anyone.",
13272
+ expiresAt: null,
13273
+ recoveryCommand: null
13274
+ };
13275
+ }
13276
+ function formatPendingReason(reason) {
13277
+ if (reason === "not_member") return "not in the conversation at send time, so the @mention was not delivered";
13278
+ return reason;
13279
+ }
13280
+ function normalizeAgentPendingMentionActions(data) {
13281
+ const value = data;
13282
+ return (Array.isArray(value?.pendingMentionActions) ? value.pendingMentionActions : Array.isArray(value?.actions) ? value.actions : Array.isArray(value?.results) ? value.results : []).filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
13283
+ resolutionId: String(item.resolutionId ?? item.id ?? ""),
13284
+ messageId: String(item.messageId ?? ""),
13285
+ targetType: String(item.targetType ?? "unknown"),
13286
+ targetHandle: String(item.targetHandle ?? ""),
13287
+ reason: String(item.reason ?? "Mention target was not notified at send time."),
13288
+ availableActions: Array.isArray(item.availableActions) ? item.availableActions.map(String) : [],
13289
+ expiresAt: typeof item.expiresAt === "string" ? item.expiresAt : null
13290
+ })).filter((item) => item.resolutionId.length > 0);
13291
+ }
13292
+ function normalizeAgentMentionActionResults(data) {
13293
+ const value = data;
13294
+ return (Array.isArray(value?.results) ? value.results : Array.isArray(value?.actionResults) ? value.actionResults : []).filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
13295
+ resolutionId: String(item.resolutionId ?? item.id ?? ""),
13296
+ status: String(item.status ?? "not_found"),
13297
+ action: typeof item.action === "string" ? normalizeAgentMentionAction(item.action) : null,
13298
+ messageId: typeof item.messageId === "string" ? item.messageId : null,
13299
+ channelId: typeof item.channelId === "string" ? item.channelId : null,
13300
+ targetType: typeof item.targetType === "string" ? item.targetType : null,
13301
+ targetId: typeof item.targetId === "string" ? item.targetId : null,
13302
+ targetHandle: typeof item.targetHandle === "string" ? item.targetHandle : null,
13303
+ message: typeof item.message === "string" ? item.message : null,
13304
+ reason: typeof item.reason === "string" ? item.reason : null,
13305
+ dedupedResolutionIds: Array.isArray(item.dedupedResolutionIds) ? item.dedupedResolutionIds.filter((id) => typeof id === "string" && id.length > 0) : []
13306
+ })).filter((item) => item.resolutionId.length > 0);
13307
+ }
13308
+ const PENDING_DEFAULT_LIMIT = 50;
13309
+ function pendingPageVerdict(hasMore, limit, shown) {
13310
+ const asked = limit === void 0 ? "server default 50" : `--limit ${limit}`;
13311
+ if (hasMore === true) return (limit ?? PENDING_DEFAULT_LIMIT) >= 100 ? `shown ${shown}, ${asked} — truncated=true · more pending actions exist AND this is the server's maximum page (100); this route has no offset or cursor, so the rest cannot be reached from here — resolve what you can and re-run; rows with no available actions are not actionable here and will remain until they expire` : `shown ${shown}, ${asked} — truncated=true · more pending actions exist; raise --limit (server caps at 100) or resolve these first and re-run`;
13312
+ if (hasMore === false) return `shown ${shown}, ${asked} — truncated=false · this is the complete list`;
13313
+ return `shown ${shown}, ${asked} — truncated=unknown · server did not report has_more, so completeness is NOT asserted`;
13314
+ }
13315
+ /** Undelivered-mentions partial result (send) or the pending list. */
13316
+ function formatAgentPendingMentionActions(actions, opts = {}) {
13317
+ const unresolvedMentionHandles = opts.source === "send" ? Array.from(new Set(opts.unresolvedMentionHandles ?? [])) : [];
13318
+ const pageNote = opts.source === "pending" ? pendingPageVerdict(opts.hasMore, opts.limit, actions.length) : "";
13319
+ if (actions.length === 0 && unresolvedMentionHandles.length === 0) return opts.source === "pending" ? `Pending mention actions\n\nNo pending mention actions. (${pageNote})\n` : "";
13320
+ if (opts.source === "send") {
13321
+ const lines = [
13322
+ "Undelivered mentions — partial result",
13323
+ "Message effect: status=queued. Queue acceptance is the only message proof.",
13324
+ "Do not rerun `raft message send`; the message is already queued and a retry could duplicate it.",
13325
+ "Each row below is bound to the literal @token from your message.",
13326
+ "For a literal name rather than a recipient, wrap the @handle in inline or fenced code.",
13327
+ ""
13328
+ ];
13329
+ for (const rawAction of actions) {
13330
+ const action = toAgentSenderPendingMentionAction(rawAction);
13331
+ lines.push(`- ${action.targetHandle} — status=${action.status}`);
13332
+ lines.push(` reason: ${action.reason}`);
13333
+ lines.push(` consequence: ${action.consequence}`);
13334
+ lines.push(` pending action: ${action.recoveryCommand ? action.resolutionId : "[invalid pending action id]"}`);
13335
+ if (action.messageId) lines.push(` message: ${action.messageId}`);
13336
+ lines.push(` expires: ${action.expiresAt ?? "unknown"}`);
13337
+ if (action.recoveryCommand) {
13338
+ lines.push(` recovery: ${action.recoveryCommand}`);
13339
+ lines.push(" note: the handle resolved, but the target was not in this conversation at send time. This does not prove the person left the server.");
13340
+ lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
13341
+ } else lines.push(" recovery: unavailable because the pending action id is invalid; inspect `raft mention pending` without resending the message.");
13342
+ }
13343
+ for (const rawHandle of unresolvedMentionHandles) {
13344
+ const warning = toAgentSenderUnresolvedMentionWarning(rawHandle);
13345
+ lines.push(`- ${warning.targetHandle} — status=${warning.status}`);
13346
+ lines.push(` reason: ${warning.reason}`);
13347
+ lines.push(` consequence: ${warning.consequence}`);
13348
+ lines.push(" pending action: none; no visible target resolved for this token");
13349
+ lines.push(" expires: n/a");
13350
+ lines.push(" recovery: if this was a literal name or prose, wrap it in inline/fenced code; otherwise verify the exact handle and send only a corrected follow-up mention; do not resend this message.");
13351
+ }
13352
+ return `${lines.join("\n")}\n`;
13353
+ }
13354
+ const lines = [
13355
+ "Pending mention actions",
13356
+ `(${pageNote})`,
13357
+ ""
13358
+ ];
13359
+ for (const action of actions) {
13360
+ const target = action.targetHandle ? `${action.targetHandle} (${action.targetType})` : action.targetType;
13361
+ lines.push(`- ${action.resolutionId} — ${target}`);
13362
+ if (action.messageId) lines.push(` message: ${action.messageId}`);
13363
+ lines.push(` reason: ${formatPendingReason(action.reason)}`);
13364
+ if (action.expiresAt) lines.push(` expires: ${action.expiresAt}`);
13365
+ const commands = formatActionCommands(action);
13366
+ if (commands.length > 0) {
13367
+ lines.push(" recovery commands:");
13368
+ lines.push(...commands);
13369
+ if (commands.some((command) => command.includes(" mention notify "))) lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
13370
+ }
13371
+ }
13372
+ return `${lines.join("\n")}\n`;
13373
+ }
13374
+ function formatMentionActionDetail(result) {
13375
+ const detail = result.message ?? result.reason ?? null;
13376
+ if (result.status === "dropped") return detail ? `not delivered: ${detail}` : "not delivered";
13377
+ return detail;
13378
+ }
13379
+ /** notify/add action outcome rows. */
13380
+ function formatAgentMentionActionResults(action, results) {
13381
+ const lines = [`Mention ${action} results`, ""];
13382
+ if (results.length === 0) {
13383
+ lines.push("No result rows returned.");
13384
+ return `${lines.join("\n")}\n`;
13385
+ }
13386
+ for (const result of results) {
13387
+ const target = result.targetHandle ? ` ${result.targetHandle}` : "";
13388
+ const detail = formatMentionActionDetail(result);
13389
+ const suffix = detail ? ` — ${detail}` : "";
13390
+ lines.push(`- ${result.resolutionId}${target}: ${result.status}${suffix}`);
13391
+ if (result.dedupedResolutionIds && result.dedupedResolutionIds.length > 1) lines.push(` deduped: ${result.dedupedResolutionIds.join(", ")}`);
13392
+ }
13393
+ if (action === "notify" && results.some((result) => result.status === "queued" && result.reason !== "already_queued")) lines.push("", `Recipient guidance: ${formatAgentReplyAffordance({ non_member_mention: true })}`);
13394
+ return `${lines.join("\n")}\n`;
13395
+ }
13396
+ function describeDelivery(row) {
13397
+ switch (row.outcome) {
13398
+ case "delivered": return "delivered";
13399
+ case "pending": return "still in flight — do not conclude yet";
13400
+ case "unknown": return "UNKNOWN — our instrument could not see this one; re-check, do not conclude";
13401
+ case "lost": switch (row.reasonCategory) {
13402
+ case "quota": return "LOST — target is rate/quota limited; it will not arrive, re-route or retry later";
13403
+ case "runtime_error": return "LOST — target's runtime rejected delivery; re-route";
13404
+ case "not_launched": return "LOST — target was not running; re-route or start it";
13405
+ case "unclassified": return "LOST — cause is outside the seat-availability set; re-route";
13406
+ default: return "LOST — server sent no category for this row (malformed); treat the cause as unknown";
13407
+ }
13408
+ }
13409
+ }
13410
+ /** Per-target mention delivery outcome for a message the bound agent sent. */
13411
+ function formatAgentSenderMentionDeliveries(messageId, deliveries) {
13412
+ if (deliveries.length === 0) return `Message ${messageId}\n\nNo mention deliveries recorded for this message.\n`;
13413
+ const lines = [`Message ${messageId} — mention delivery per target`, ""];
13414
+ for (const row of deliveries) lines.push(` ${row.targetHandle || "(unknown target)"}: ${describeDelivery(row)}`);
13415
+ return `${lines.join("\n")}\n`;
13416
+ }
13417
+ //#endregion
13418
+ //#region ../shared/src/agentOps/mentions.ts
13419
+ async function pendingMentionActions(client, request = {}) {
13420
+ const result = await client.mentions.pendingActions(request.limit === void 0 ? {} : { limit: String(request.limit) });
13421
+ if (!result.ok) return failureFromClientResult(result);
13422
+ const actions = normalizeAgentPendingMentionActions(result.data);
13423
+ const hasMore = typeof result.data.has_more === "boolean" ? result.data.has_more : null;
13424
+ const first = actions.find((a) => a.availableActions.length > 0);
13425
+ const next = first ? {
13426
+ kind: "resolve_mention",
13427
+ command: `raft mention notify ${first.resolutionId}`,
13428
+ args: {
13429
+ action: "notify",
13430
+ resolutionIds: [first.resolutionId]
13431
+ },
13432
+ why: "This @mention reached nobody at send time; notify the target (or add them) so the message is seen."
13433
+ } : null;
13434
+ return {
13435
+ ok: true,
13436
+ state: actions.length > 0 ? "pending" : "empty",
13437
+ data: {
13438
+ actions,
13439
+ hasMore,
13440
+ limit: request.limit
13441
+ },
13442
+ next,
13443
+ text: formatAgentPendingMentionActions(actions, {
13444
+ source: "pending",
13445
+ ...hasMore === null ? {} : { hasMore },
13446
+ limit: request.limit
13447
+ })
13448
+ };
13449
+ }
13450
+ async function executeMentionAction(client, request) {
13451
+ const ids = (request.resolutionIds ?? []).map((id) => id.trim()).filter(Boolean);
13452
+ if (ids.length === 0 || request.action !== "notify" && request.action !== "add") return failureOutcome(opError("INVALID_REQUEST", { message: "Pass an action (notify | add) and at least one resolution id." }));
13453
+ const result = await client.mentions.executeAction({
13454
+ action: request.action,
13455
+ resolutionIds: ids
13456
+ });
13492
13457
  if (!result.ok) return failureFromClientResult(result);
13493
- const verb = action === "add" ? "added to" : "removed from";
13458
+ const results = normalizeAgentMentionActionResults(result.data);
13494
13459
  return {
13495
13460
  ok: true,
13496
- state: action === "add" ? "added" : "removed",
13461
+ state: "executed",
13497
13462
  data: {
13498
- messageId: request.messageId,
13499
- emoji
13463
+ action: request.action,
13464
+ results
13500
13465
  },
13501
13466
  next: null,
13502
- text: `Reaction ${emoji} ${verb} message ${request.messageId.slice(0, 8)}.`
13467
+ text: formatAgentMentionActionResults(request.action, results)
13468
+ };
13469
+ }
13470
+ async function senderMentionDeliveries(client, request) {
13471
+ if (!request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id is required." }));
13472
+ const result = await client.mentions.senderDeliveries({ messageId: request.messageId });
13473
+ if (!result.ok) return failureFromClientResult(result);
13474
+ const data = result.data;
13475
+ return {
13476
+ ok: true,
13477
+ state: data.deliveries.length > 0 ? "deliveries" : "empty",
13478
+ data,
13479
+ next: null,
13480
+ text: formatAgentSenderMentionDeliveries(data.messageId, data.deliveries)
13503
13481
  };
13504
13482
  }
13505
13483
  //#endregion
13506
- //#region ../shared/src/agentText/mentions.ts
13507
- function normalizeAgentMentionAction(action) {
13508
- if (action === "notify" || action === "notify_only") return "notify";
13509
- if (action === "add" || action === "invite") return "add";
13510
- return null;
13484
+ //#region ../shared/src/agentText/knowledge.ts
13485
+ function formatMatchReason(result) {
13486
+ const parts = [];
13487
+ for (const { term, matched } of result.correctedTerms ?? []) parts.push(`${term} → ${matched} (typo)`);
13488
+ for (const { from, to } of result.expandedTerms ?? []) parts.push(`${from} → ${to} (concept)`);
13489
+ return parts.length > 0 ? ` matched: ${parts.join(", ")}` : "";
13511
13490
  }
13512
- function formatActionCommands(action) {
13513
- const verbs = action.availableActions.map(normalizeAgentMentionAction).filter((verb) => verb !== null);
13514
- return Array.from(new Set(verbs)).map((verb) => ` ${verb}: raft mention ${verb} ${action.resolutionId}`);
13491
+ function formatAgentKnowledgeSearchResults(results) {
13492
+ return results.map((result, index) => {
13493
+ const body = result.firstScreen.trim().split(/\r?\n/).filter(Boolean).map((line) => ` ${line}`).join("\n");
13494
+ const reason = formatMatchReason(result);
13495
+ return `${index + 1}. ${result.slug} — ${result.title}${reason ? `\n${reason}` : ""}${body ? `\n${body}` : ""}`;
13496
+ }).join("\n\n") + "\n";
13515
13497
  }
13516
- function formatAuthoredMentionToken(targetHandle) {
13517
- return targetHandle.startsWith("@") ? targetHandle : `@${targetHandle}`;
13498
+ function formatAgentKnowledgeStdout(content) {
13499
+ return content.endsWith("\n") ? content : `${content}\n`;
13518
13500
  }
13519
- const PENDING_MENTION_ACTION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13520
- /** Per-token mention recovery command line; null when the id is not a UUID (fail closed). */
13521
- function formatAgentMentionNotifyRecoveryCommand(resolutionId) {
13522
- return PENDING_MENTION_ACTION_ID_RE.test(resolutionId) ? `raft mention notify ${resolutionId}` : null;
13501
+ //#endregion
13502
+ //#region ../shared/src/agentOps/manual.ts
13503
+ function requireContext(context) {
13504
+ if (!context?.intent?.trim() || !context?.reason?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Manual calls need a short `intent` and `reason`." }));
13505
+ return null;
13523
13506
  }
13524
- function toAgentSenderPendingMentionAction(action) {
13507
+ async function getManualTopic(client, request) {
13508
+ const invalid = requireContext(request);
13509
+ if (invalid) return invalid;
13510
+ if (!request.topic?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A topic id is required." }));
13511
+ const result = await client.knowledge.get({
13512
+ topic: request.topic.trim(),
13513
+ intent: request.intent.trim(),
13514
+ reason: request.reason.trim()
13515
+ });
13516
+ if (!result.ok) return failureFromClientResult(result);
13525
13517
  return {
13526
- resolutionId: action.resolutionId,
13527
- messageId: action.messageId,
13528
- targetHandle: formatAuthoredMentionToken(action.targetHandle),
13529
- status: "not_queued",
13530
- reason: "not_in_conversation",
13531
- consequence: "This @mention did not notify anyone.",
13532
- expiresAt: action.expiresAt ?? null,
13533
- recoveryCommand: formatAgentMentionNotifyRecoveryCommand(action.resolutionId)
13518
+ ok: true,
13519
+ state: "topic",
13520
+ data: result.data,
13521
+ next: null,
13522
+ text: formatAgentKnowledgeStdout(result.data.content)
13534
13523
  };
13535
13524
  }
13536
- function toAgentSenderUnresolvedMentionWarning(targetHandle) {
13525
+ async function searchManual(client, request) {
13526
+ const invalid = requireContext(request);
13527
+ if (invalid) return invalid;
13528
+ if (!request.query?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Search keywords are required." }));
13529
+ const result = await client.knowledge.search({
13530
+ query: request.query.trim(),
13531
+ ...request.scope ? { scope: request.scope } : {},
13532
+ intent: request.intent.trim(),
13533
+ reason: request.reason.trim()
13534
+ });
13535
+ if (!result.ok) return failureFromClientResult(result);
13536
+ const data = result.data;
13537
+ const results = data.results;
13538
+ const first = results[0];
13539
+ const next = first ? {
13540
+ kind: "read_manual_topic",
13541
+ command: `raft manual get ${first.slug} --intent "…" --reason "…"`,
13542
+ args: { topic: first.slug },
13543
+ why: "Open the best-matching topic."
13544
+ } : null;
13537
13545
  return {
13538
- targetHandle: formatAuthoredMentionToken(targetHandle),
13539
- status: "not_queued",
13540
- reason: "unknown_or_not_visible",
13541
- consequence: "This @mention did not notify anyone.",
13542
- expiresAt: null,
13543
- recoveryCommand: null
13546
+ ok: true,
13547
+ state: results.length > 0 ? "results" : "empty",
13548
+ data,
13549
+ next,
13550
+ text: results.length > 0 ? formatAgentKnowledgeSearchResults(results) : "No Manual topics matched. Retry with different keywords, keeping your intent and reason.\n"
13544
13551
  };
13545
13552
  }
13546
- function formatPendingReason(reason) {
13547
- if (reason === "not_member") return "not in the conversation at send time, so the @mention was not delivered";
13548
- return reason;
13549
- }
13550
- function normalizeAgentPendingMentionActions(data) {
13551
- const value = data;
13552
- return (Array.isArray(value?.pendingMentionActions) ? value.pendingMentionActions : Array.isArray(value?.actions) ? value.actions : Array.isArray(value?.results) ? value.results : []).filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
13553
- resolutionId: String(item.resolutionId ?? item.id ?? ""),
13554
- messageId: String(item.messageId ?? ""),
13555
- targetType: String(item.targetType ?? "unknown"),
13556
- targetHandle: String(item.targetHandle ?? ""),
13557
- reason: String(item.reason ?? "Mention target was not notified at send time."),
13558
- availableActions: Array.isArray(item.availableActions) ? item.availableActions.map(String) : [],
13559
- expiresAt: typeof item.expiresAt === "string" ? item.expiresAt : null
13560
- })).filter((item) => item.resolutionId.length > 0);
13553
+ //#endregion
13554
+ //#region ../shared/src/agentOps/state.ts
13555
+ const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
13556
+ const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
13557
+ const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
13558
+ function isNullableCursor(value) {
13559
+ return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
13561
13560
  }
13562
- function normalizeAgentMentionActionResults(data) {
13563
- const value = data;
13564
- return (Array.isArray(value?.results) ? value.results : Array.isArray(value?.actionResults) ? value.actionResults : []).filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
13565
- resolutionId: String(item.resolutionId ?? item.id ?? ""),
13566
- status: String(item.status ?? "not_found"),
13567
- action: typeof item.action === "string" ? normalizeAgentMentionAction(item.action) : null,
13568
- messageId: typeof item.messageId === "string" ? item.messageId : null,
13569
- channelId: typeof item.channelId === "string" ? item.channelId : null,
13570
- targetType: typeof item.targetType === "string" ? item.targetType : null,
13571
- targetId: typeof item.targetId === "string" ? item.targetId : null,
13572
- targetHandle: typeof item.targetHandle === "string" ? item.targetHandle : null,
13573
- message: typeof item.message === "string" ? item.message : null,
13574
- reason: typeof item.reason === "string" ? item.reason : null,
13575
- dedupedResolutionIds: Array.isArray(item.dedupedResolutionIds) ? item.dedupedResolutionIds.filter((id) => typeof id === "string" && id.length > 0) : []
13576
- })).filter((item) => item.resolutionId.length > 0);
13561
+ /** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
13562
+ function parseRaftState(value) {
13563
+ if (!value || typeof value !== "object") return null;
13564
+ const v = value;
13565
+ if (v.schema !== "raft-sdk-state.v1") return null;
13566
+ if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
13567
+ if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
13568
+ const frontier = v.frontier;
13569
+ const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
13570
+ const r = c;
13571
+ return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
13572
+ }) : [];
13573
+ return {
13574
+ schema: RAFT_STATE_SCHEMA,
13575
+ version: v.version,
13576
+ cursor: v.cursor ?? null,
13577
+ pendingCursor: v.pendingCursor ?? null,
13578
+ frontier: frontier && frontier.version === 1 ? frontier : {
13579
+ version: 1,
13580
+ targets: {},
13581
+ aliases: {}
13582
+ },
13583
+ continuations
13584
+ };
13577
13585
  }
13578
- const PENDING_DEFAULT_LIMIT = 50;
13579
- function pendingPageVerdict(hasMore, limit, shown) {
13580
- const asked = limit === void 0 ? "server default 50" : `--limit ${limit}`;
13581
- if (hasMore === true) return (limit ?? PENDING_DEFAULT_LIMIT) >= 100 ? `shown ${shown}, ${asked} — truncated=true · more pending actions exist AND this is the server's maximum page (100); this route has no offset or cursor, so the rest cannot be reached from here — resolve what you can and re-run; rows with no available actions are not actionable here and will remain until they expire` : `shown ${shown}, ${asked} — truncated=true · more pending actions exist; raise --limit (server caps at 100) or resolve these first and re-run`;
13582
- if (hasMore === false) return `shown ${shown}, ${asked} — truncated=false · this is the complete list`;
13583
- return `shown ${shown}, ${asked} — truncated=unknown · server did not report has_more, so completeness is NOT asserted`;
13586
+ /** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
13587
+ async function hashRaftSendContent(target, content, attachmentIds = []) {
13588
+ const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
13589
+ return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
13584
13590
  }
13585
- /** Undelivered-mentions partial result (send) or the pending list. */
13586
- function formatAgentPendingMentionActions(actions, opts = {}) {
13587
- const unresolvedMentionHandles = opts.source === "send" ? Array.from(new Set(opts.unresolvedMentionHandles ?? [])) : [];
13588
- const pageNote = opts.source === "pending" ? pendingPageVerdict(opts.hasMore, opts.limit, actions.length) : "";
13589
- if (actions.length === 0 && unresolvedMentionHandles.length === 0) return opts.source === "pending" ? `Pending mention actions\n\nNo pending mention actions. (${pageNote})\n` : "";
13590
- if (opts.source === "send") {
13591
- const lines = [
13592
- "Undelivered mentions — partial result",
13593
- "Message effect: status=queued. Queue acceptance is the only message proof.",
13594
- "Do not rerun `raft message send`; the message is already queued and a retry could duplicate it.",
13595
- "Each row below is bound to the literal @token from your message.",
13596
- "For a literal name rather than a recipient, wrap the @handle in inline or fenced code.",
13597
- ""
13598
- ];
13599
- for (const rawAction of actions) {
13600
- const action = toAgentSenderPendingMentionAction(rawAction);
13601
- lines.push(`- ${action.targetHandle} — status=${action.status}`);
13602
- lines.push(` reason: ${action.reason}`);
13603
- lines.push(` consequence: ${action.consequence}`);
13604
- lines.push(` pending action: ${action.recoveryCommand ? action.resolutionId : "[invalid pending action id]"}`);
13605
- if (action.messageId) lines.push(` message: ${action.messageId}`);
13606
- lines.push(` expires: ${action.expiresAt ?? "unknown"}`);
13607
- if (action.recoveryCommand) {
13608
- lines.push(` recovery: ${action.recoveryCommand}`);
13609
- lines.push(" note: the handle resolved, but the target was not in this conversation at send time. This does not prove the person left the server.");
13610
- lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
13611
- } else lines.push(" recovery: unavailable because the pending action id is invalid; inspect `raft mention pending` without resending the message.");
13612
- }
13613
- for (const rawHandle of unresolvedMentionHandles) {
13614
- const warning = toAgentSenderUnresolvedMentionWarning(rawHandle);
13615
- lines.push(`- ${warning.targetHandle} — status=${warning.status}`);
13616
- lines.push(` reason: ${warning.reason}`);
13617
- lines.push(` consequence: ${warning.consequence}`);
13618
- lines.push(" pending action: none; no visible target resolved for this token");
13619
- lines.push(" expires: n/a");
13620
- lines.push(" recovery: if this was a literal name or prose, wrap it in inline/fenced code; otherwise verify the exact handle and send only a corrected follow-up mention; do not resend this message.");
13621
- }
13622
- return `${lines.join("\n")}\n`;
13591
+ /**
13592
+ * One client's view of the persisted state: loads once, holds the working
13593
+ * copy, saves on request. Also used without a store (memory only), so the
13594
+ * pending/committed cursor flow behaves the same with or without persistence.
13595
+ */
13596
+ var RaftStateSession = class {
13597
+ store;
13598
+ onError;
13599
+ frontier;
13600
+ cursor = null;
13601
+ pendingCursor = null;
13602
+ continuations = [];
13603
+ loadedVersion = void 0;
13604
+ loading = null;
13605
+ dirty = false;
13606
+ constructor(store, frontier, onError) {
13607
+ this.store = store;
13608
+ this.onError = onError;
13609
+ this.frontier = frontier;
13610
+ }
13611
+ get persistent() {
13612
+ return this.store !== void 0;
13613
+ }
13614
+ /** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
13615
+ get version() {
13616
+ return this.loadedVersion;
13623
13617
  }
13624
- const lines = [
13625
- "Pending mention actions",
13626
- `(${pageNote})`,
13627
- ""
13628
- ];
13629
- for (const action of actions) {
13630
- const target = action.targetHandle ? `${action.targetHandle} (${action.targetType})` : action.targetType;
13631
- lines.push(`- ${action.resolutionId} — ${target}`);
13632
- if (action.messageId) lines.push(` message: ${action.messageId}`);
13633
- lines.push(` reason: ${formatPendingReason(action.reason)}`);
13634
- if (action.expiresAt) lines.push(` expires: ${action.expiresAt}`);
13635
- const commands = formatActionCommands(action);
13636
- if (commands.length > 0) {
13637
- lines.push(" recovery commands:");
13638
- lines.push(...commands);
13639
- if (commands.some((command) => command.includes(" mention notify "))) lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
13640
- }
13618
+ /** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
13619
+ ensureLoaded() {
13620
+ if (!this.store) return Promise.resolve();
13621
+ this.loading ??= (async () => {
13622
+ let raw = null;
13623
+ try {
13624
+ raw = await this.store.load();
13625
+ } catch (error) {
13626
+ this.onError?.(error, {
13627
+ phase: "load",
13628
+ version: void 0
13629
+ });
13630
+ return;
13631
+ }
13632
+ const state = parseRaftState(raw);
13633
+ if (!state) return;
13634
+ this.loadedVersion = state.version;
13635
+ this.cursor = state.cursor;
13636
+ this.pendingCursor = state.pendingCursor;
13637
+ this.frontier.absorb(state.frontier);
13638
+ this.continuations = state.continuations ?? [];
13639
+ })();
13640
+ return this.loading;
13641
13641
  }
13642
- return `${lines.join("\n")}\n`;
13643
- }
13644
- function formatMentionActionDetail(result) {
13645
- const detail = result.message ?? result.reason ?? null;
13646
- if (result.status === "dropped") return detail ? `not delivered: ${detail}` : "not delivered";
13647
- return detail;
13648
- }
13649
- /** notify/add action outcome rows. */
13650
- function formatAgentMentionActionResults(action, results) {
13651
- const lines = [`Mention ${action} results`, ""];
13652
- if (results.length === 0) {
13653
- lines.push("No result rows returned.");
13654
- return `${lines.join("\n")}\n`;
13642
+ markDirty() {
13643
+ this.dirty = true;
13655
13644
  }
13656
- for (const result of results) {
13657
- const target = result.targetHandle ? ` ${result.targetHandle}` : "";
13658
- const detail = formatMentionActionDetail(result);
13659
- const suffix = detail ? ` — ${detail}` : "";
13660
- lines.push(`- ${result.resolutionId}${target}: ${result.status}${suffix}`);
13661
- if (result.dedupedResolutionIds && result.dedupedResolutionIds.length > 1) lines.push(` deduped: ${result.dedupedResolutionIds.join(", ")}`);
13645
+ /** Record the cursor of a batch just returned. Acknowledges nothing. */
13646
+ setPending(cursor) {
13647
+ if (cursor === null || cursor === this.pendingCursor) return;
13648
+ this.pendingCursor = cursor;
13649
+ this.dirty = true;
13662
13650
  }
13663
- if (action === "notify" && results.some((result) => result.status === "queued" && result.reason !== "already_queued")) lines.push("", `Recipient guidance: ${formatAgentReplyAffordance({ non_member_mention: true })}`);
13664
- return `${lines.join("\n")}\n`;
13665
- }
13666
- function describeDelivery(row) {
13667
- switch (row.outcome) {
13668
- case "delivered": return "delivered";
13669
- case "pending": return "still in flight — do not conclude yet";
13670
- case "unknown": return "UNKNOWN — our instrument could not see this one; re-check, do not conclude";
13671
- case "lost": switch (row.reasonCategory) {
13672
- case "quota": return "LOST — target is rate/quota limited; it will not arrive, re-route or retry later";
13673
- case "runtime_error": return "LOST — target's runtime rejected delivery; re-route";
13674
- case "not_launched": return "LOST — target was not running; re-route or start it";
13675
- case "unclassified": return "LOST — cause is outside the seat-availability set; re-route";
13676
- default: return "LOST — server sent no category for this row (malformed); treat the cause as unknown";
13651
+ /**
13652
+ * Promote a cursor to committed. With no argument, commits `pendingCursor`.
13653
+ * Returns the committed cursor, or null when there was nothing to commit.
13654
+ * Never moves the committed cursor backwards.
13655
+ */
13656
+ commit(cursor) {
13657
+ const next = cursor === void 0 ? this.pendingCursor : cursor;
13658
+ if (next === null || next === void 0) return null;
13659
+ if (this.cursor === null || next > this.cursor) this.cursor = next;
13660
+ if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
13661
+ this.dirty = true;
13662
+ return this.cursor;
13663
+ }
13664
+ findContinuation(target, contentHash) {
13665
+ return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
13666
+ }
13667
+ rememberContinuation(entry) {
13668
+ this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
13669
+ this.continuations.push(entry);
13670
+ const perTarget = this.continuations.filter((c) => c.target === entry.target);
13671
+ for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
13672
+ if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
13673
+ this.dirty = true;
13674
+ }
13675
+ forgetContinuation(target, contentHash) {
13676
+ const before = this.continuations.length;
13677
+ this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
13678
+ if (this.continuations.length !== before) this.dirty = true;
13679
+ }
13680
+ snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
13681
+ return {
13682
+ schema: RAFT_STATE_SCHEMA,
13683
+ version: nextVersion,
13684
+ cursor: this.cursor,
13685
+ pendingCursor: this.pendingCursor,
13686
+ frontier: this.frontier.snapshot(),
13687
+ continuations: [...this.continuations]
13688
+ };
13689
+ }
13690
+ /**
13691
+ * Save if anything changed (or `force`). One attempt; a failure is reported
13692
+ * and swallowed. Returns whether the save succeeded (true when nothing to save).
13693
+ */
13694
+ async save(options = {}) {
13695
+ if (!this.store) return true;
13696
+ if (!this.dirty && !options.force) return true;
13697
+ const expectedVersion = this.loadedVersion;
13698
+ const state = this.snapshot();
13699
+ try {
13700
+ await this.store.save(state, { expectedVersion });
13701
+ } catch (error) {
13702
+ this.onError?.(error, {
13703
+ phase: "save",
13704
+ version: expectedVersion
13705
+ });
13706
+ return false;
13677
13707
  }
13708
+ this.loadedVersion = state.version;
13709
+ this.dirty = false;
13710
+ return true;
13678
13711
  }
13712
+ };
13713
+ //#endregion
13714
+ //#region ../shared/src/agentOps/actions.ts
13715
+ /** Text is the CLI's `raft action prepare` output. */
13716
+ function formatAgentActionCardPosted(target, messageId) {
13717
+ const shortId = messageId ? messageId.slice(0, 8) : null;
13718
+ return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
13679
13719
  }
13680
- /** Per-target mention delivery outcome for a message the bound agent sent. */
13681
- function formatAgentSenderMentionDeliveries(messageId, deliveries) {
13682
- if (deliveries.length === 0) return `Message ${messageId}\n\nNo mention deliveries recorded for this message.\n`;
13683
- const lines = [`Message ${messageId} — mention delivery per target`, ""];
13684
- for (const row of deliveries) lines.push(` ${row.targetHandle || "(unknown target)"}: ${describeDelivery(row)}`);
13685
- return `${lines.join("\n")}\n`;
13720
+ async function prepareActionCard(client, request) {
13721
+ if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
13722
+ const result = await client.actions.prepare(request);
13723
+ if (!result.ok) return failureFromClientResult(result);
13724
+ const card = {
13725
+ target: request.target,
13726
+ messageId: result.data.messageId
13727
+ };
13728
+ return {
13729
+ ok: true,
13730
+ state: "prepared",
13731
+ data: card,
13732
+ next: {
13733
+ kind: "await_confirmation",
13734
+ command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
13735
+ args: {
13736
+ target: request.target,
13737
+ messageId: card.messageId
13738
+ },
13739
+ why: "A human must click the card to commit it. You are not notified of the result yet; read the card message to see its state instead of waiting."
13740
+ },
13741
+ text: formatAgentActionCardPosted(request.target, card.messageId)
13742
+ };
13686
13743
  }
13687
13744
  //#endregion
13688
- //#region ../shared/src/agentOps/mentions.ts
13689
- async function pendingMentionActions(client, request = {}) {
13690
- const result = await client.mentions.pendingActions(request.limit === void 0 ? {} : { limit: String(request.limit) });
13745
+ //#region src/command.ts
13746
+ /** Run one `raft` command on the Server for the agent this credential belongs to. */
13747
+ async function runCommand(routes, request) {
13748
+ const result = await routes.request("agentCommand", { body: request });
13691
13749
  if (!result.ok) return failureFromClientResult(result);
13692
- const actions = normalizeAgentPendingMentionActions(result.data);
13693
- const hasMore = typeof result.data.has_more === "boolean" ? result.data.has_more : null;
13694
- const first = actions.find((a) => a.availableActions.length > 0);
13695
- const next = first ? {
13696
- kind: "resolve_mention",
13697
- command: `raft mention notify ${first.resolutionId}`,
13698
- args: {
13699
- action: "notify",
13700
- resolutionIds: [first.resolutionId]
13701
- },
13702
- why: "This @mention reached nobody at send time; notify the target (or add them) so the message is seen."
13703
- } : null;
13704
13750
  return {
13705
13751
  ok: true,
13706
- state: actions.length > 0 ? "pending" : "empty",
13707
- data: {
13708
- actions,
13709
- hasMore,
13710
- limit: request.limit
13711
- },
13712
- next,
13713
- text: formatAgentPendingMentionActions(actions, {
13714
- source: "pending",
13715
- ...hasMore === null ? {} : { hasMore },
13716
- limit: request.limit
13717
- })
13752
+ exitCode: result.data.exitCode,
13753
+ text: result.data.text,
13754
+ outcome: result.data.outcome
13718
13755
  };
13719
13756
  }
13720
- async function executeMentionAction(client, request) {
13721
- const ids = (request.resolutionIds ?? []).map((id) => id.trim()).filter(Boolean);
13722
- if (ids.length === 0 || request.action !== "notify" && request.action !== "add") return failureOutcome(opError("INVALID_REQUEST", { message: "Pass an action (notify | add) and at least one resolution id." }));
13723
- const result = await client.mentions.executeAction({
13724
- action: request.action,
13725
- resolutionIds: ids
13757
+ //#endregion
13758
+ //#region src/client.ts
13759
+ var RaftSdkConfigurationError = class extends Error {
13760
+ code;
13761
+ name = "RaftSdkConfigurationError";
13762
+ constructor(code, message) {
13763
+ super(message);
13764
+ this.code = code;
13765
+ }
13766
+ };
13767
+ function requireServerUrl(value) {
13768
+ const serverUrl = value.trim();
13769
+ if (!serverUrl) throw new RaftSdkConfigurationError("MISSING_SERVER_URL", "Raft Server URL is required");
13770
+ let parsed;
13771
+ try {
13772
+ parsed = new URL(serverUrl);
13773
+ } catch {
13774
+ throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must be an absolute HTTP(S) URL");
13775
+ }
13776
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must use HTTP or HTTPS");
13777
+ if (parsed.username || parsed.password) throw new RaftSdkConfigurationError("INVALID_SERVER_URL", "Raft Server URL must not contain credentials");
13778
+ return serverUrl.replace(/\/+$/, "");
13779
+ }
13780
+ function requireAgentCredential(value) {
13781
+ const credential = value.trim();
13782
+ if (!credential) throw new RaftSdkConfigurationError("MISSING_AGENT_CREDENTIAL", "Raft Agent credential is required");
13783
+ if (!credential.startsWith("sk_agent_") || credential.length === 9) throw new RaftSdkConfigurationError("INVALID_AGENT_CREDENTIAL", "Raft Agent credential must be a long-lived External Agent credential");
13784
+ return credential;
13785
+ }
13786
+ /**
13787
+ * Create a typed client for the credential-derived `/internal/agent-api/*`
13788
+ * surface. The SDK does not read CLI profiles, environment variables, or the
13789
+ * host user's home directory: the caller chooses and supplies the credential.
13790
+ */
13791
+ function createRaftClient(options) {
13792
+ const serverUrl = requireServerUrl(options.serverUrl);
13793
+ const auth = { authorization: `Bearer ${requireAgentCredential(options.credential)}` };
13794
+ const messageClient = createAgentApiMessageClient({
13795
+ baseUrl: serverUrl,
13796
+ fetch: options.fetch,
13797
+ headers: options.headers,
13798
+ retry: options.retry,
13799
+ throttle: options.throttle,
13800
+ auth
13801
+ });
13802
+ const agentApi = createAgentApiClient({ fetch: {
13803
+ baseUrl: serverUrl,
13804
+ fetch: options.fetch,
13805
+ headers: options.headers,
13806
+ retry: { attempts: Math.min(Math.max(1, options.retry?.attempts ?? 1), 5) },
13807
+ throttle: options.throttle,
13808
+ auth
13809
+ } });
13810
+ const writeApi = createAgentApiClient({ fetch: {
13811
+ baseUrl: serverUrl,
13812
+ fetch: options.fetch,
13813
+ headers: options.headers,
13814
+ retry: { attempts: 1 },
13815
+ throttle: options.throttle,
13816
+ auth
13817
+ } });
13818
+ const eventsApi = createAgentApiClient({ fetch: {
13819
+ baseUrl: serverUrl,
13820
+ fetch: (input, init) => (options.fetch ?? fetch)(input, {
13821
+ ...init,
13822
+ cache: "no-store",
13823
+ redirect: "error"
13824
+ }),
13825
+ headers: Object.fromEntries(new Headers(options.headers)),
13826
+ retry: { attempts: 1 },
13827
+ throttle: options.throttle,
13828
+ auth
13829
+ } });
13830
+ const routes = createRaftRoutes({
13831
+ serverUrl,
13832
+ fetch: options.fetch,
13833
+ headers: options.headers,
13834
+ authorization: auth.authorization,
13835
+ readAttempts: options.retry?.attempts,
13836
+ beforeRequest: options.throttle?.beforeRequest
13726
13837
  });
13727
- if (!result.ok) return failureFromClientResult(result);
13728
- const results = normalizeAgentMentionActionResults(result.data);
13729
- return {
13730
- ok: true,
13731
- state: "executed",
13732
- data: {
13733
- action: request.action,
13734
- results
13735
- },
13736
- next: null,
13737
- text: formatAgentMentionActionResults(request.action, results)
13738
- };
13739
- }
13740
- async function senderMentionDeliveries(client, request) {
13741
- if (!request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id is required." }));
13742
- const result = await client.mentions.senderDeliveries({ messageId: request.messageId });
13743
- if (!result.ok) return failureFromClientResult(result);
13744
- const data = result.data;
13745
13838
  return {
13746
- ok: true,
13747
- state: data.deliveries.length > 0 ? "deliveries" : "empty",
13748
- data,
13749
- next: null,
13750
- text: formatAgentSenderMentionDeliveries(data.messageId, data.deliveries)
13839
+ ...messageClient,
13840
+ ...createRaftManageClient(agentApi, writeApi, {
13841
+ serverUrl,
13842
+ fetch: options.fetch ?? fetch,
13843
+ headers: Object.fromEntries(new Headers(options.headers)),
13844
+ authorization: auth.authorization,
13845
+ beforeRequest: options.throttle?.beforeRequest
13846
+ }),
13847
+ events: { receive: (request) => receiveRaftEvents(eventsApi, request) },
13848
+ channels: { join: (request) => joinRaftChannelByTarget(agentApi, request) },
13849
+ routes,
13850
+ runCommand: (request) => runCommand(routes, request),
13851
+ agent: { context: () => getRaftContext(agentApi) }
13751
13852
  };
13752
13853
  }
13753
13854
  //#endregion
13754
- //#region ../shared/src/agentText/knowledge.ts
13755
- function formatMatchReason(result) {
13756
- const parts = [];
13757
- for (const { term, matched } of result.correctedTerms ?? []) parts.push(`${term} → ${matched} (typo)`);
13758
- for (const { from, to } of result.expandedTerms ?? []) parts.push(`${from} → ${to} (concept)`);
13759
- return parts.length > 0 ? ` matched: ${parts.join(", ")}` : "";
13760
- }
13761
- function formatAgentKnowledgeSearchResults(results) {
13762
- return results.map((result, index) => {
13763
- const body = result.firstScreen.trim().split(/\r?\n/).filter(Boolean).map((line) => ` ${line}`).join("\n");
13764
- const reason = formatMatchReason(result);
13765
- return `${index + 1}. ${result.slug} — ${result.title}${reason ? `\n${reason}` : ""}${body ? `\n${body}` : ""}`;
13766
- }).join("\n\n") + "\n";
13855
+ //#region ../shared/src/clock.ts
13856
+ function currentTimeMs() {
13857
+ return Date.now();
13767
13858
  }
13768
- function formatAgentKnowledgeStdout(content) {
13769
- return content.endsWith("\n") ? content : `${content}\n`;
13859
+ function currentDate() {
13860
+ return new Date(currentTimeMs());
13770
13861
  }
13771
13862
  //#endregion
13772
- //#region ../shared/src/agentOps/manual.ts
13773
- function requireContext(context) {
13774
- if (!context?.intent?.trim() || !context?.reason?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Manual calls need a short `intent` and `reason`." }));
13775
- return null;
13863
+ //#region src/credential.ts
13864
+ var RaftCredentialError = class extends Error {
13865
+ code;
13866
+ name = "RaftCredentialError";
13867
+ constructor(code, message) {
13868
+ super(message);
13869
+ this.code = code;
13870
+ }
13871
+ };
13872
+ function isNonEmptyString(value) {
13873
+ return typeof value === "string" && value.trim().length > 0;
13776
13874
  }
13777
- async function getManualTopic(client, request) {
13778
- const invalid = requireContext(request);
13779
- if (invalid) return invalid;
13780
- if (!request.topic?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A topic id is required." }));
13781
- const result = await client.knowledge.get({
13782
- topic: request.topic.trim(),
13783
- intent: request.intent.trim(),
13784
- reason: request.reason.trim()
13785
- });
13786
- if (!result.ok) return failureFromClientResult(result);
13875
+ function parseIdentityResponse(value) {
13876
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
13877
+ const input = value;
13878
+ if (!isNonEmptyString(input.agentId) || !isNonEmptyString(input.agentName) || input.agentDisplayName !== null && typeof input.agentDisplayName !== "string" || !isNonEmptyString(input.serverId) || !isNonEmptyString(input.credentialId) || !Array.isArray(input.scopes) || !input.scopes.every(isNonEmptyString)) return null;
13787
13879
  return {
13788
- ok: true,
13789
- state: "topic",
13790
- data: result.data,
13791
- next: null,
13792
- text: formatAgentKnowledgeStdout(result.data.content)
13880
+ agentId: input.agentId,
13881
+ agentName: input.agentName,
13882
+ agentDisplayName: input.agentDisplayName,
13883
+ serverId: input.serverId,
13884
+ credentialId: input.credentialId,
13885
+ scopes: [...input.scopes]
13793
13886
  };
13794
13887
  }
13795
- async function searchManual(client, request) {
13796
- const invalid = requireContext(request);
13797
- if (invalid) return invalid;
13798
- if (!request.query?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Search keywords are required." }));
13799
- const result = await client.knowledge.search({
13800
- query: request.query.trim(),
13801
- ...request.scope ? { scope: request.scope } : {},
13802
- intent: request.intent.trim(),
13803
- reason: request.reason.trim()
13804
- });
13805
- if (!result.ok) return failureFromClientResult(result);
13806
- const data = result.data;
13807
- const results = data.results;
13808
- const first = results[0];
13809
- const next = first ? {
13810
- kind: "read_manual_topic",
13811
- command: `raft manual get ${first.slug} --intent "…" --reason "…"`,
13812
- args: { topic: first.slug },
13813
- why: "Open the best-matching topic."
13814
- } : null;
13888
+ function parseStoredRaftCredential(value) {
13889
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
13890
+ const input = value;
13891
+ if (input.schemaVersion !== 1 || !isNonEmptyString(input.serverUrl) || !isNonEmptyString(input.credential) || !isNonEmptyString(input.storedAt)) return null;
13892
+ const identity = parseIdentityResponse(input);
13893
+ if (!identity || Number.isNaN(Date.parse(input.storedAt))) return null;
13894
+ let serverUrl;
13895
+ let credential;
13896
+ try {
13897
+ serverUrl = requireServerUrl(input.serverUrl);
13898
+ credential = requireAgentCredential(input.credential);
13899
+ } catch {
13900
+ return null;
13901
+ }
13815
13902
  return {
13816
- ok: true,
13817
- state: results.length > 0 ? "results" : "empty",
13818
- data,
13819
- next,
13820
- text: results.length > 0 ? formatAgentKnowledgeSearchResults(results) : "No Manual topics matched. Retry with different keywords, keeping your intent and reason.\n"
13903
+ schemaVersion: 1,
13904
+ serverUrl,
13905
+ credential,
13906
+ storedAt: input.storedAt,
13907
+ ...identity
13821
13908
  };
13822
13909
  }
13823
- //#endregion
13824
- //#region ../shared/src/agentOps/state.ts
13825
- const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
13826
- const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
13827
- const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
13828
- function isNullableCursor(value) {
13829
- return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
13830
- }
13831
- /** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
13832
- function parseRaftState(value) {
13833
- if (!value || typeof value !== "object") return null;
13834
- const v = value;
13835
- if (v.schema !== "raft-sdk-state.v1") return null;
13836
- if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
13837
- if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
13838
- const frontier = v.frontier;
13839
- const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
13840
- const r = c;
13841
- return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
13842
- }) : [];
13843
- return {
13844
- schema: RAFT_STATE_SCHEMA,
13845
- version: v.version,
13846
- cursor: v.cursor ?? null,
13847
- pendingCursor: v.pendingCursor ?? null,
13848
- frontier: frontier && frontier.version === 1 ? frontier : {
13849
- version: 1,
13850
- targets: {},
13851
- aliases: {}
13852
- },
13853
- continuations
13910
+ /**
13911
+ * Validate an existing External Agent credential against Raft, derive its
13912
+ * bound identity from the credential-authenticated `whoami` surface, and save
13913
+ * it through the caller's explicit store. This does not mint or rotate a
13914
+ * credential and never returns the credential bytes.
13915
+ */
13916
+ async function bootstrapRaftCredential(options) {
13917
+ const serverUrl = requireServerUrl(options.serverUrl);
13918
+ const credential = requireAgentCredential(options.credential);
13919
+ const httpFetch = options.fetch ?? globalThis.fetch;
13920
+ let existing;
13921
+ try {
13922
+ existing = await options.store.load();
13923
+ } catch {
13924
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential could not be checked before saving");
13925
+ }
13926
+ if (existing) {
13927
+ const validatedExisting = parseStoredRaftCredential(existing);
13928
+ if (!validatedExisting) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential record is invalid");
13929
+ existing = validatedExisting;
13930
+ if (existing.serverUrl !== serverUrl || existing.credential !== credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
13931
+ }
13932
+ let response;
13933
+ try {
13934
+ response = await httpFetch(`${serverUrl}/internal/agent-api/`, {
13935
+ method: "GET",
13936
+ headers: { authorization: `Bearer ${credential}` }
13937
+ });
13938
+ } catch {
13939
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", "Could not reach the Raft Server to verify the Agent credential");
13940
+ }
13941
+ if (!response.ok) {
13942
+ if (response.status === 401 || response.status === 403) throw new RaftCredentialError("CREDENTIAL_REJECTED", "The Raft Server rejected the Agent credential");
13943
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", `The Raft Server could not verify the Agent credential (status ${response.status})`);
13944
+ }
13945
+ let payload;
13946
+ try {
13947
+ payload = await response.json();
13948
+ } catch {
13949
+ throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
13950
+ }
13951
+ const identity = parseIdentityResponse(payload);
13952
+ if (!identity) throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
13953
+ if (!identity.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The Raft Agent credential does not include the send capability required by this SDK");
13954
+ if (existing && (existing.serverId !== identity.serverId || existing.agentId !== identity.agentId)) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store is already bound to a different Raft Agent identity");
13955
+ if (existing) return {
13956
+ serverUrl,
13957
+ ...identity
13958
+ };
13959
+ const record = {
13960
+ schemaVersion: 1,
13961
+ serverUrl,
13962
+ credential,
13963
+ storedAt: currentDate().toISOString(),
13964
+ ...identity
13965
+ };
13966
+ try {
13967
+ await options.store.save(record);
13968
+ } catch (error) {
13969
+ if (error instanceof RaftCredentialError && error.code === "CREDENTIAL_STORE_CONFLICT") throw error;
13970
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The verified Agent credential could not be saved");
13971
+ }
13972
+ return {
13973
+ serverUrl,
13974
+ ...identity
13854
13975
  };
13855
13976
  }
13856
- /** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
13857
- async function hashRaftSendContent(target, content, attachmentIds = []) {
13858
- const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
13859
- return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
13977
+ /** Load the caller-selected credential store and create a normal Raft client. */
13978
+ async function createRaftClientFromStore(options) {
13979
+ let record;
13980
+ try {
13981
+ record = await options.store.load();
13982
+ } catch {
13983
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential could not be loaded");
13984
+ }
13985
+ if (!record) throw new RaftCredentialError("CREDENTIAL_NOT_FOUND", "No stored Raft Agent credential was found");
13986
+ const validated = parseStoredRaftCredential(record);
13987
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential is invalid");
13988
+ if (!validated.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The stored Raft Agent credential does not include the send capability required by this SDK");
13989
+ return createRaftClient({
13990
+ serverUrl: validated.serverUrl,
13991
+ credential: validated.credential,
13992
+ fetch: options.fetch,
13993
+ headers: options.headers,
13994
+ retry: options.retry,
13995
+ throttle: options.throttle
13996
+ });
13860
13997
  }
13998
+ //#endregion
13999
+ //#region src/fileCredentialStore.ts
14000
+ const MAX_CREDENTIAL_FILE_BYTES = 65536;
13861
14001
  /**
13862
- * One client's view of the persisted state: loads once, holds the working
13863
- * copy, saves on request. Also used without a store (memory only), so the
13864
- * pending/committed cursor flow behaves the same with or without persistence.
14002
+ * Create a Node.js credential store at one explicit absolute path. The SDK
14003
+ * never discovers CLI profiles or a home directory. Writes use a same-directory
14004
+ * temporary file followed by atomic rename, and the final file is mode 0600.
13865
14005
  */
13866
- var RaftStateSession = class {
13867
- store;
13868
- onError;
13869
- frontier;
13870
- cursor = null;
13871
- pendingCursor = null;
13872
- continuations = [];
13873
- loadedVersion = void 0;
13874
- loading = null;
13875
- dirty = false;
13876
- constructor(store, frontier, onError) {
13877
- this.store = store;
13878
- this.onError = onError;
13879
- this.frontier = frontier;
13880
- }
13881
- get persistent() {
13882
- return this.store !== void 0;
13883
- }
13884
- /** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
13885
- get version() {
13886
- return this.loadedVersion;
13887
- }
13888
- /** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
13889
- ensureLoaded() {
13890
- if (!this.store) return Promise.resolve();
13891
- this.loading ??= (async () => {
13892
- let raw = null;
14006
+ function createFileCredentialStore(filePath) {
14007
+ if (!filePath.trim() || !(0, node_path.isAbsolute)(filePath)) throw new RaftSdkConfigurationError("INVALID_CREDENTIAL_PATH", "Raft credential file path must be an explicit absolute path");
14008
+ return {
14009
+ load: () => loadFileCredential(filePath),
14010
+ async save(record) {
14011
+ const validated = parseStoredRaftCredential(record);
14012
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "Refusing to write an invalid Raft Agent credential record");
14013
+ const directory = (0, node_path.dirname)(filePath);
14014
+ const temporaryPath = `${filePath}.${process.pid}.${(0, node_crypto.randomUUID)()}.tmp`;
14015
+ let handle;
13893
14016
  try {
13894
- raw = await this.store.load();
13895
- } catch (error) {
13896
- this.onError?.(error, {
13897
- phase: "load",
13898
- version: void 0
14017
+ await (0, node_fs_promises.mkdir)(directory, {
14018
+ recursive: true,
14019
+ mode: 448
13899
14020
  });
13900
- return;
14021
+ const existing = await loadFileCredential(filePath);
14022
+ if (existing) {
14023
+ assertSameCredential(existing, validated);
14024
+ return;
14025
+ }
14026
+ handle = await (0, node_fs_promises.open)(temporaryPath, "wx", 384);
14027
+ await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`, "utf8");
14028
+ await handle.sync();
14029
+ await (0, node_fs_promises.chmod)(temporaryPath, 384);
14030
+ await handle.close();
14031
+ handle = void 0;
14032
+ try {
14033
+ await (0, node_fs_promises.link)(temporaryPath, filePath);
14034
+ } catch (error) {
14035
+ if (error.code !== "EEXIST") throw error;
14036
+ const winner = await loadFileCredential(filePath);
14037
+ if (!winner) throw error;
14038
+ assertSameCredential(winner, validated);
14039
+ }
14040
+ await (0, node_fs_promises.unlink)(temporaryPath);
14041
+ } catch (error) {
14042
+ if (handle) await handle.close().catch(() => void 0);
14043
+ await (0, node_fs_promises.unlink)(temporaryPath).catch(() => void 0);
14044
+ if (error instanceof RaftCredentialError) throw error;
14045
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The Raft Agent credential could not be written atomically");
13901
14046
  }
13902
- const state = parseRaftState(raw);
13903
- if (!state) return;
13904
- this.loadedVersion = state.version;
13905
- this.cursor = state.cursor;
13906
- this.pendingCursor = state.pendingCursor;
13907
- this.frontier.absorb(state.frontier);
13908
- this.continuations = state.continuations ?? [];
13909
- })();
13910
- return this.loading;
13911
- }
13912
- markDirty() {
13913
- this.dirty = true;
13914
- }
13915
- /** Record the cursor of a batch just returned. Acknowledges nothing. */
13916
- setPending(cursor) {
13917
- if (cursor === null || cursor === this.pendingCursor) return;
13918
- this.pendingCursor = cursor;
13919
- this.dirty = true;
13920
- }
13921
- /**
13922
- * Promote a cursor to committed. With no argument, commits `pendingCursor`.
13923
- * Returns the committed cursor, or null when there was nothing to commit.
13924
- * Never moves the committed cursor backwards.
13925
- */
13926
- commit(cursor) {
13927
- const next = cursor === void 0 ? this.pendingCursor : cursor;
13928
- if (next === null || next === void 0) return null;
13929
- if (this.cursor === null || next > this.cursor) this.cursor = next;
13930
- if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
13931
- this.dirty = true;
13932
- return this.cursor;
13933
- }
13934
- findContinuation(target, contentHash) {
13935
- return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
13936
- }
13937
- rememberContinuation(entry) {
13938
- this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
13939
- this.continuations.push(entry);
13940
- const perTarget = this.continuations.filter((c) => c.target === entry.target);
13941
- for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
13942
- if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
13943
- this.dirty = true;
13944
- }
13945
- forgetContinuation(target, contentHash) {
13946
- const before = this.continuations.length;
13947
- this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
13948
- if (this.continuations.length !== before) this.dirty = true;
13949
- }
13950
- snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
13951
- return {
13952
- schema: RAFT_STATE_SCHEMA,
13953
- version: nextVersion,
13954
- cursor: this.cursor,
13955
- pendingCursor: this.pendingCursor,
13956
- frontier: this.frontier.snapshot(),
13957
- continuations: [...this.continuations]
13958
- };
13959
- }
13960
- /**
13961
- * Save if anything changed (or `force`). One attempt; a failure is reported
13962
- * and swallowed. Returns whether the save succeeded (true when nothing to save).
13963
- */
13964
- async save(options = {}) {
13965
- if (!this.store) return true;
13966
- if (!this.dirty && !options.force) return true;
13967
- const expectedVersion = this.loadedVersion;
13968
- const state = this.snapshot();
13969
- try {
13970
- await this.store.save(state, { expectedVersion });
13971
- } catch (error) {
13972
- this.onError?.(error, {
13973
- phase: "save",
13974
- version: expectedVersion
13975
- });
13976
- return false;
13977
14047
  }
13978
- this.loadedVersion = state.version;
13979
- this.dirty = false;
13980
- return true;
14048
+ };
14049
+ }
14050
+ async function loadFileCredential(filePath) {
14051
+ let initialMetadata;
14052
+ try {
14053
+ initialMetadata = await (0, node_fs_promises.lstat)(filePath);
14054
+ } catch (error) {
14055
+ if (error.code === "ENOENT") return null;
14056
+ throw fileReadError();
13981
14057
  }
13982
- };
13983
- //#endregion
13984
- //#region ../shared/src/agentOps/actions.ts
13985
- /** Text is the CLI's `raft action prepare` output. */
13986
- function formatAgentActionCardPosted(target, messageId) {
13987
- const shortId = messageId ? messageId.slice(0, 8) : null;
13988
- return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
14058
+ if (!initialMetadata.isFile() || initialMetadata.isSymbolicLink() || initialMetadata.size > MAX_CREDENTIAL_FILE_BYTES) throw fileReadError();
14059
+ let parsed;
14060
+ let handle;
14061
+ try {
14062
+ const noFollow = process.platform === "win32" ? 0 : node_fs.constants.O_NOFOLLOW;
14063
+ handle = await (0, node_fs_promises.open)(filePath, node_fs.constants.O_RDONLY | noFollow);
14064
+ const openedMetadata = await handle.stat();
14065
+ if (!openedMetadata.isFile() || openedMetadata.size > MAX_CREDENTIAL_FILE_BYTES || initialMetadata.dev !== openedMetadata.dev || initialMetadata.ino !== openedMetadata.ino) throw fileReadError();
14066
+ if (process.platform !== "win32" && (openedMetadata.mode & 63) !== 0) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file permissions are too broad; expected mode 0600");
14067
+ parsed = JSON.parse(await handle.readFile("utf8"));
14068
+ } catch (error) {
14069
+ if (error instanceof RaftCredentialError) throw error;
14070
+ throw fileReadError();
14071
+ } finally {
14072
+ await handle?.close().catch(() => void 0);
14073
+ }
14074
+ const record = parseStoredRaftCredential(parsed);
14075
+ if (!record) throw fileReadError();
14076
+ return record;
13989
14077
  }
13990
- async function prepareActionCard(client, request) {
13991
- if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
13992
- const result = await client.actions.prepare(request);
13993
- if (!result.ok) return failureFromClientResult(result);
13994
- const card = {
13995
- target: request.target,
13996
- messageId: result.data.messageId
13997
- };
13998
- return {
13999
- ok: true,
14000
- state: "prepared",
14001
- data: card,
14002
- next: {
14003
- kind: "await_confirmation",
14004
- command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
14005
- args: {
14006
- target: request.target,
14007
- messageId: card.messageId
14008
- },
14009
- why: "A human must click the card to commit it. You are not notified of the result yet; read the card message to see its state instead of waiting."
14010
- },
14011
- text: formatAgentActionCardPosted(request.target, card.messageId)
14012
- };
14078
+ function assertSameCredential(existing, candidate) {
14079
+ if (existing.credential !== candidate.credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
14080
+ }
14081
+ function fileReadError() {
14082
+ return new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file is invalid or unreadable");
14013
14083
  }
14014
14084
  //#endregion
14015
14085
  //#region src/latestReadThread.ts
@@ -14243,7 +14313,8 @@ function createRaft(options) {
14243
14313
  load: () => session.ensureLoaded(),
14244
14314
  snapshot: () => session.snapshot(session.version ?? 0)
14245
14315
  },
14246
- routes
14316
+ routes,
14317
+ runCommand: (request) => runCommand(routes, request)
14247
14318
  };
14248
14319
  }
14249
14320
  //#endregion
@@ -14274,4 +14345,5 @@ exports.parseRaftState = parseRaftState;
14274
14345
  exports.projectRaftMessage = projectRaftMessage;
14275
14346
  exports.projectRaftMessages = projectRaftMessages;
14276
14347
  exports.readLatestReadThread = readLatestReadThread;
14348
+ exports.runCommand = runCommand;
14277
14349
  exports.verifyInboxNotice = verifyInboxNotice;