@botiverse/raft-sdk 0.3.0 → 1.0.0-alpha.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.
- package/README.md +50 -170
- package/dist/cjs/index.cjs +94 -2393
- package/dist/esm/index.js +95 -2385
- package/dist/index.d.ts +50 -499
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -7390,13 +7390,8 @@ function encodeQuery(routeKey, query) {
|
|
|
7390
7390
|
return params;
|
|
7391
7391
|
}
|
|
7392
7392
|
function parseBody(routeKey, body) {
|
|
7393
|
+
if (body === void 0) return void 0;
|
|
7393
7394
|
const route = agentApiContract[routeKey];
|
|
7394
|
-
if (body === void 0) {
|
|
7395
|
-
if (!("body" in route.request)) return void 0;
|
|
7396
|
-
const schema = route.request.body;
|
|
7397
|
-
if (schema.safeParse(void 0).success || schema.safeParse({}).success) return void 0;
|
|
7398
|
-
return failure$3(routeKey, "request_contract_mismatch", `Agent API ${route.key} requires a request body`);
|
|
7399
|
-
}
|
|
7400
7395
|
try {
|
|
7401
7396
|
return "body" in route.request ? route.request.body.parse(body) : void 0;
|
|
7402
7397
|
} catch (cause) {
|
|
@@ -7501,19 +7496,7 @@ function createAgentApiRawClient(transport, options = {}) {
|
|
|
7501
7496
|
const routeKey = route.key;
|
|
7502
7497
|
const { resource, method } = route.client;
|
|
7503
7498
|
const resourceClient = client[resource] ??= {};
|
|
7504
|
-
|
|
7505
|
-
"params",
|
|
7506
|
-
"query",
|
|
7507
|
-
"body"
|
|
7508
|
-
].filter((part) => part in route.request).length;
|
|
7509
|
-
resourceClient[method] = (...args) => {
|
|
7510
|
-
if (args.length > expectedArgs) return Promise.resolve(failure$3(routeKey, "request_contract_mismatch", `Agent API ${route.key} takes ${expectedArgs} argument${expectedArgs === 1 ? "" : "s"} (${[
|
|
7511
|
-
"params",
|
|
7512
|
-
"query",
|
|
7513
|
-
"body"
|
|
7514
|
-
].filter((part) => part in route.request).join(", ") || "none"}); got ${args.length}`));
|
|
7515
|
-
return requestAgentApiRawRoute(transport, routeKey, requestOptionsFromMethodArgs(routeKey, pathPrefix, args));
|
|
7516
|
-
};
|
|
7499
|
+
resourceClient[method] = (...args) => requestAgentApiRawRoute(transport, routeKey, requestOptionsFromMethodArgs(routeKey, pathPrefix, args));
|
|
7517
7500
|
}
|
|
7518
7501
|
return client;
|
|
7519
7502
|
}
|
|
@@ -9904,7 +9887,7 @@ function clampAttempts(value) {
|
|
|
9904
9887
|
* The split is decided per route from `AGENT_API_ROUTE_META`, never by the
|
|
9905
9888
|
* caller and never by the HTTP method.
|
|
9906
9889
|
*/
|
|
9907
|
-
function
|
|
9890
|
+
function createRaftRoutes(options) {
|
|
9908
9891
|
const base = {
|
|
9909
9892
|
baseUrl: options.serverUrl,
|
|
9910
9893
|
fetch: options.fetch,
|
|
@@ -9931,61 +9914,12 @@ function createRaftRouteClient(options) {
|
|
|
9931
9914
|
...merged,
|
|
9932
9915
|
request: (routeKey, requestOptions) => {
|
|
9933
9916
|
return (ROUTE_INFO[routeKey].retryPolicy === "retry" ? retrying : single).request(routeKey, requestOptions);
|
|
9934
|
-
}
|
|
9935
|
-
};
|
|
9936
|
-
}
|
|
9937
|
-
const ROUTE_PARTS = [
|
|
9938
|
-
"params",
|
|
9939
|
-
"query",
|
|
9940
|
-
"body"
|
|
9941
|
-
];
|
|
9942
|
-
function inputMismatch(routeKey, message) {
|
|
9943
|
-
return Promise.resolve({
|
|
9944
|
-
ok: false,
|
|
9945
|
-
routeKey,
|
|
9946
|
-
error: {
|
|
9947
|
-
kind: "validation",
|
|
9948
|
-
reason: "request_contract_mismatch",
|
|
9949
|
-
message: `Agent API ${routeKey} ${message}`
|
|
9950
|
-
}
|
|
9951
|
-
});
|
|
9952
|
-
}
|
|
9953
|
-
/**
|
|
9954
|
-
* The public routes layer: one named-object call shape for every route,
|
|
9955
|
-
* `routes.<resource>.<method>({ params, query, body })`. The types reject a
|
|
9956
|
-
* missing required part or a part the route does not have; for callers
|
|
9957
|
-
* without type checking, the same rules are enforced at runtime and nothing
|
|
9958
|
-
* is sent on a mismatch.
|
|
9959
|
-
*/
|
|
9960
|
-
function raftRoutesFromClient(client) {
|
|
9961
|
-
const call = (routeKey, args) => {
|
|
9962
|
-
if (args.length > 1) return inputMismatch(routeKey, `takes one { params, query, body } object; got ${args.length} arguments`);
|
|
9963
|
-
const input = args[0];
|
|
9964
|
-
if (input !== void 0 && (input === null || typeof input !== "object" || Array.isArray(input))) return inputMismatch(routeKey, "takes one { params, query, body } object");
|
|
9965
|
-
const route = agentApiContract[routeKey];
|
|
9966
|
-
const declared = ROUTE_PARTS.filter((part) => part in route.request);
|
|
9967
|
-
const unknown = (input === void 0 ? [] : Object.keys(input)).filter((key) => !declared.includes(key));
|
|
9968
|
-
if (unknown.length > 0) return inputMismatch(routeKey, `has no ${unknown.join(", ")} (it takes ${declared.join(", ") || "no input"})`);
|
|
9969
|
-
return client.request(routeKey, input ?? {});
|
|
9970
|
-
};
|
|
9971
|
-
const methods = {};
|
|
9972
|
-
for (const route of Object.values(agentApiContract)) {
|
|
9973
|
-
const key = route.key;
|
|
9974
|
-
methods[route.client.resource] ??= {};
|
|
9975
|
-
methods[route.client.resource][route.client.method] = (...args) => call(key, args);
|
|
9976
|
-
}
|
|
9977
|
-
return {
|
|
9978
|
-
...methods,
|
|
9979
|
-
request: (routeKey, ...rest) => call(routeKey, rest),
|
|
9917
|
+
},
|
|
9980
9918
|
manifestVersion: AGENT_API_MANIFEST_VERSION,
|
|
9981
9919
|
describe: describeRaftRoute,
|
|
9982
9920
|
list: listRaftRoutes
|
|
9983
9921
|
};
|
|
9984
9922
|
}
|
|
9985
|
-
/** Build the public routes layer directly (convenience over createRaftRouteClient + raftRoutesFromClient). */
|
|
9986
|
-
function createRaftRoutes(options) {
|
|
9987
|
-
return raftRoutesFromClient(createRaftRouteClient(options));
|
|
9988
|
-
}
|
|
9989
9923
|
//#endregion
|
|
9990
9924
|
//#region src/context.ts
|
|
9991
9925
|
const projection = object({
|
|
@@ -11041,22 +10975,7 @@ function toAgentMessageLike(envelope) {
|
|
|
11041
10975
|
task_current_projection: envelope.task_current_projection ?? envelope.taskCurrentProjection ?? null
|
|
11042
10976
|
};
|
|
11043
10977
|
}
|
|
11044
|
-
/**
|
|
11045
|
-
* An envelope carries a reply target only when it names its conversation
|
|
11046
|
-
* (`channel_type` + `channel_name`, or a third-party event). Some Server
|
|
11047
|
-
* responses embed bare envelopes (for example `recentUnread: [{ content }]`);
|
|
11048
|
-
* those must not be rendered with a made-up target.
|
|
11049
|
-
*/
|
|
11050
|
-
function hasAgentMessageIdentity(envelope) {
|
|
11051
|
-
const like = toAgentMessageLike(envelope);
|
|
11052
|
-
if (like.third_party_event) return true;
|
|
11053
|
-
if (typeof like.channel_type !== "string" || typeof like.channel_name !== "string" || !like.channel_name) return false;
|
|
11054
|
-
if (like.channel_type === "thread" && !like.parent_channel_name) return false;
|
|
11055
|
-
return true;
|
|
11056
|
-
}
|
|
11057
|
-
/** Project one envelope; `null` when it has no conversation identity (skip it rather than render `#undefined`). */
|
|
11058
10978
|
function projectRaftMessage(envelope) {
|
|
11059
|
-
if (!hasAgentMessageIdentity(envelope)) return null;
|
|
11060
10979
|
const like = toAgentMessageLike(envelope);
|
|
11061
10980
|
const id = nullableString(like.message_id);
|
|
11062
10981
|
const taskNumber = nullableNumber(like.task_number);
|
|
@@ -11095,10 +11014,6 @@ function projectRaftMessage(envelope) {
|
|
|
11095
11014
|
function sortBySeq(messages) {
|
|
11096
11015
|
return [...messages].sort((a, b) => (a.seq ?? Number.MAX_SAFE_INTEGER) - (b.seq ?? Number.MAX_SAFE_INTEGER));
|
|
11097
11016
|
}
|
|
11098
|
-
/** Project a list, skipping envelopes without a conversation identity. */
|
|
11099
|
-
function projectRaftMessages(envelopes) {
|
|
11100
|
-
return envelopes.map(projectRaftMessage).filter((m) => m !== null);
|
|
11101
|
-
}
|
|
11102
11017
|
//#endregion
|
|
11103
11018
|
//#region ../shared/src/agentOps/frontier.ts
|
|
11104
11019
|
const MAX_EXACT_SEQS = 2500;
|
|
@@ -11110,17 +11025,13 @@ var SeenFrontier = class SeenFrontier {
|
|
|
11110
11025
|
aliases = /* @__PURE__ */ new Map();
|
|
11111
11026
|
static fromSnapshot(snapshot) {
|
|
11112
11027
|
const frontier = new SeenFrontier();
|
|
11113
|
-
|
|
11114
|
-
return frontier;
|
|
11115
|
-
}
|
|
11116
|
-
/** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
|
|
11117
|
-
absorb(snapshot) {
|
|
11118
|
-
if (!snapshot || snapshot.version !== 1) return;
|
|
11119
|
-
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) this.recordAlias(alias, canonical);
|
|
11028
|
+
if (!snapshot || snapshot.version !== 1) return frontier;
|
|
11120
11029
|
for (const [target, state] of Object.entries(snapshot.targets ?? {})) {
|
|
11121
|
-
if (positiveInt(state.upTo))
|
|
11122
|
-
if (Array.isArray(state.exact))
|
|
11030
|
+
if (positiveInt(state.upTo)) frontier.recordUpTo(target, state.upTo);
|
|
11031
|
+
if (Array.isArray(state.exact)) frontier.recordExact(target, state.exact);
|
|
11123
11032
|
}
|
|
11033
|
+
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) frontier.recordAlias(alias, canonical);
|
|
11034
|
+
return frontier;
|
|
11124
11035
|
}
|
|
11125
11036
|
/** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
|
|
11126
11037
|
recordAlias(requested, canonical) {
|
|
@@ -11150,17 +11061,6 @@ var SeenFrontier = class SeenFrontier {
|
|
|
11150
11061
|
for (const seq of keep) state.exact.add(seq);
|
|
11151
11062
|
}
|
|
11152
11063
|
}
|
|
11153
|
-
/**
|
|
11154
|
-
* Attest that the model saw the context a held send/claim returned. Call it
|
|
11155
|
-
* only after the held messages actually reached the model (the SDK cannot
|
|
11156
|
-
* know that, so it never records this implicitly). Returns false and records
|
|
11157
|
-
* nothing when the context was withheld or the Server sent no boundary.
|
|
11158
|
-
*/
|
|
11159
|
-
recordHeld(held) {
|
|
11160
|
-
if (held.withheld || held.seenUpToSeq === null) return false;
|
|
11161
|
-
this.recordUpTo(held.target, held.seenUpToSeq);
|
|
11162
|
-
return true;
|
|
11163
|
-
}
|
|
11164
11064
|
/** What to attest on a send or claim to `target`; omits `seenUpToSeq` when nothing contiguous is known. */
|
|
11165
11065
|
attestation(target) {
|
|
11166
11066
|
const state = this.targets.get(this.canonical(target));
|
|
@@ -11239,7 +11139,7 @@ async function checkInbox(client, request = {}, frontier) {
|
|
|
11239
11139
|
});
|
|
11240
11140
|
if (!result.ok) return failureFromClientResult(result);
|
|
11241
11141
|
const data = result.data;
|
|
11242
|
-
const messages = sortBySeq(
|
|
11142
|
+
const messages = sortBySeq(data.events.map(projectRaftMessage));
|
|
11243
11143
|
recordExactSeen(frontier, messages);
|
|
11244
11144
|
const batch = {
|
|
11245
11145
|
messages,
|
|
@@ -11267,7 +11167,7 @@ async function checkInbox(client, request = {}, frontier) {
|
|
|
11267
11167
|
* return value summarises the drain; the batch that ends the iteration is
|
|
11268
11168
|
* acknowledged only by the caller's next pull (`summary.cursor`).
|
|
11269
11169
|
*/
|
|
11270
|
-
async function* drainInbox(client, request = {}, frontier
|
|
11170
|
+
async function* drainInbox(client, request = {}, frontier) {
|
|
11271
11171
|
const ack = request.ack ?? "cursor";
|
|
11272
11172
|
let cursor = request.since ?? null;
|
|
11273
11173
|
let ackMode = ack;
|
|
@@ -11290,7 +11190,6 @@ async function* drainInbox(client, request = {}, frontier, onPull) {
|
|
|
11290
11190
|
error: round
|
|
11291
11191
|
};
|
|
11292
11192
|
const batch = round.data;
|
|
11293
|
-
await onPull?.(sent, batch);
|
|
11294
11193
|
ackMode = batch.ackMode;
|
|
11295
11194
|
stillUnread = batch.stillUnreadConversations;
|
|
11296
11195
|
if (batch.messages.length > 0) {
|
|
@@ -11462,7 +11361,7 @@ async function readHistory(client, request, frontier) {
|
|
|
11462
11361
|
const data = result.data;
|
|
11463
11362
|
const page = {
|
|
11464
11363
|
target: typeof data.target === "string" && data.target ? data.target : request.target,
|
|
11465
|
-
messages: sortBySeq(
|
|
11364
|
+
messages: sortBySeq(data.messages.map(projectRaftMessage)),
|
|
11466
11365
|
hasOlder: data.has_older === true,
|
|
11467
11366
|
hasNewer: data.has_newer === true,
|
|
11468
11367
|
lastReadSeq: typeof data.last_read_seq === "number" ? data.last_read_seq : null,
|
|
@@ -11492,31 +11391,25 @@ function heldText(held, action) {
|
|
|
11492
11391
|
}
|
|
11493
11392
|
function heldNext(held) {
|
|
11494
11393
|
return {
|
|
11495
|
-
kind: "
|
|
11394
|
+
kind: "review_then_resend",
|
|
11496
11395
|
command: `raft message read --target "${held.target}"`,
|
|
11497
11396
|
args: {
|
|
11498
11397
|
target: held.target,
|
|
11499
|
-
|
|
11398
|
+
newMessageCount: held.newMessageCount
|
|
11500
11399
|
},
|
|
11501
|
-
why:
|
|
11400
|
+
why: "Newer messages arrived in this conversation; read them, then resend if the message still applies."
|
|
11502
11401
|
};
|
|
11503
11402
|
}
|
|
11504
|
-
function projectHeld(target, data
|
|
11403
|
+
function projectHeld(target, data) {
|
|
11505
11404
|
const withheld = data.freshnessContextMode === "withheld";
|
|
11506
|
-
const heldMessages =
|
|
11507
|
-
const seenUpToSeq = typeof data.seenUpToSeq === "number" ? data.seenUpToSeq : null;
|
|
11405
|
+
const heldMessages = (data.heldMessages ?? []).map(projectRaftMessage);
|
|
11508
11406
|
return {
|
|
11509
11407
|
target,
|
|
11510
|
-
idempotencyKey,
|
|
11511
|
-
continuation: {
|
|
11512
|
-
idempotencyKey,
|
|
11513
|
-
...!withheld && seenUpToSeq !== null ? { seen: { upToSeq: seenUpToSeq } } : {}
|
|
11514
|
-
},
|
|
11515
11408
|
newMessageCount: withheld ? data.withheldMessageCount ?? 0 : data.newMessageCount ?? heldMessages.length,
|
|
11516
11409
|
heldMessages,
|
|
11517
11410
|
omittedMessageCount: data.omittedMessageCount ?? 0,
|
|
11518
11411
|
formalMentionCount: data.mentionAnnotation?.formalMentionCount ?? 0,
|
|
11519
|
-
seenUpToSeq,
|
|
11412
|
+
seenUpToSeq: typeof data.seenUpToSeq === "number" ? data.seenUpToSeq : null,
|
|
11520
11413
|
withheld
|
|
11521
11414
|
};
|
|
11522
11415
|
}
|
|
@@ -11528,7 +11421,7 @@ function sentOutcome(target, data) {
|
|
|
11528
11421
|
messageId: data.messageId,
|
|
11529
11422
|
messageSeq: typeof data.messageSeq === "number" ? data.messageSeq : null,
|
|
11530
11423
|
unresolvedMentionHandles: data.unresolvedMentionHandles ?? [],
|
|
11531
|
-
recentUnread:
|
|
11424
|
+
recentUnread: (data.recentUnread ?? []).map(projectRaftMessage)
|
|
11532
11425
|
};
|
|
11533
11426
|
const warn = sent.unresolvedMentionHandles.length > 0 ? ` Unresolved @handles: ${sent.unresolvedMentionHandles.map((h) => `@${h}`).join(", ")}.` : "";
|
|
11534
11427
|
return {
|
|
@@ -11575,7 +11468,7 @@ async function sendMessage(client, request, frontier, internal = {}) {
|
|
|
11575
11468
|
idempotencyKey
|
|
11576
11469
|
};
|
|
11577
11470
|
const held = {
|
|
11578
|
-
...projectHeld(request.target, data
|
|
11471
|
+
...projectHeld(request.target, data),
|
|
11579
11472
|
resend: (options) => sendMessage(client, options.seen === "held" && typeof data.seenUpToSeq === "number" ? {
|
|
11580
11473
|
...keyed,
|
|
11581
11474
|
seen: {
|
|
@@ -11599,173 +11492,12 @@ async function sendMessage(client, request, frontier, internal = {}) {
|
|
|
11599
11492
|
if (data.state === "sent") return sentOutcome(request.target, data);
|
|
11600
11493
|
return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
|
|
11601
11494
|
}
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
"in_review",
|
|
11609
|
-
"done",
|
|
11610
|
-
"closed"
|
|
11611
|
-
];
|
|
11612
|
-
function oneLineTaskTitle(title) {
|
|
11613
|
-
return (title ?? "").replace(/\s+/g, " ").trim();
|
|
11614
|
-
}
|
|
11615
|
-
/** Channel task board listing. */
|
|
11616
|
-
function formatAgentTaskList(channel, data, statusFilter) {
|
|
11617
|
-
if (!data.tasks || data.tasks.length === 0) return `No${statusFilter && statusFilter !== "all" ? ` ${statusFilter}` : ""} tasks in ${channel}.`;
|
|
11618
|
-
const formatted = data.tasks.map((t) => {
|
|
11619
|
-
const assignee = t.claimedById ? ` → ${t.claimedByName ? `@${t.claimedByName}` : "<unresolved>"}` : "";
|
|
11620
|
-
const departedCreator = t.createdByMembershipStatus === "left" || t.createdByMembershipStatus === "removed";
|
|
11621
|
-
const creator = t.createdByName ? ` (by @${t.createdByName}${departedCreator ? " [departed]" : ""})` : "";
|
|
11622
|
-
const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
|
|
11623
|
-
const revision = Number.isInteger(t.revision) ? ` rev=${t.revision}` : "";
|
|
11624
|
-
const legacy = t.isLegacy ? " [LEGACY — read-only]" : "";
|
|
11625
|
-
const resourceReceipt = t.requiresResourceReceipt ? ` resource-receipt=${t.resourceReceiptRecordedAt ? "recorded" : "pending"}` : "";
|
|
11626
|
-
const details = t.description ? `\n Current description: ${t.description.replace(/\n/g, `\n${" ".repeat(23)}`)}` : "";
|
|
11627
|
-
return `#${t.taskNumber} [${t.status}]${assignee}${creator}${msgId}${revision}${resourceReceipt}${taskTimestamps(t)}${legacy} Current title: ${oneLineTaskTitle(t.title)}${details}`;
|
|
11628
|
-
}).join("\n");
|
|
11629
|
-
return `## Task Board for ${channel} (${data.tasks.length} tasks)\n\n${formatted}`;
|
|
11630
|
-
}
|
|
11631
|
-
/** Cross-channel --mine task listing with coverage notes. */
|
|
11632
|
-
function formatAgentMyTaskList(data, statusFilter) {
|
|
11633
|
-
const tasks = data.tasks ?? [];
|
|
11634
|
-
const visibleTypes = data.coverage?.visibleChannelTypes?.join("|") ?? "unknown";
|
|
11635
|
-
const archived = data.coverage?.includesArchived ? "included" : "not included";
|
|
11636
|
-
const coverage = [
|
|
11637
|
-
`Coverage: ${data.coverage?.status ?? "unknown"}`,
|
|
11638
|
-
`visible types=${visibleTypes}`,
|
|
11639
|
-
`archived=${archived}`,
|
|
11640
|
-
`inaccessible scope=${data.coverage?.inaccessibleScope ?? "unknown"}`
|
|
11641
|
-
].join(" · ");
|
|
11642
|
-
const output = `Output: showing ${tasks.length} of ${tasks.length} visible matches · mode=${data.pagination?.mode ?? "unknown"} · truncated=${String(data.pagination?.truncated ?? "unknown")}`;
|
|
11643
|
-
const title = `## My assigned tasks on this server${statusFilter ? ` (status=${statusFilter})` : " (unfinished)"}`;
|
|
11644
|
-
if (tasks.length === 0) return [
|
|
11645
|
-
title,
|
|
11646
|
-
"",
|
|
11647
|
-
coverage,
|
|
11648
|
-
output,
|
|
11649
|
-
"",
|
|
11650
|
-
"No tasks matched in the covered visible scope."
|
|
11651
|
-
].join("\n");
|
|
11652
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
11653
|
-
for (const task of tasks) {
|
|
11654
|
-
const status = task.status ?? "unknown";
|
|
11655
|
-
const bucket = grouped.get(status) ?? [];
|
|
11656
|
-
bucket.push(task);
|
|
11657
|
-
grouped.set(status, bucket);
|
|
11658
|
-
}
|
|
11659
|
-
return [
|
|
11660
|
-
title,
|
|
11661
|
-
"",
|
|
11662
|
-
coverage,
|
|
11663
|
-
output,
|
|
11664
|
-
"",
|
|
11665
|
-
...[...TASK_STATUS_ORDER.filter((status) => grouped.has(status)), ...[...grouped.keys()].filter((status) => !TASK_STATUS_ORDER.includes(status)).sort()].map((status) => {
|
|
11666
|
-
const rows = grouped.get(status);
|
|
11667
|
-
const rendered = rows.map((task) => {
|
|
11668
|
-
const channelRef = task.channelRef ?? "<unresolved-visible-target>";
|
|
11669
|
-
const taskNumber = task.taskNumber ?? "?";
|
|
11670
|
-
const departedCreator = task.createdByMembershipStatus === "left" || task.createdByMembershipStatus === "removed";
|
|
11671
|
-
const creator = task.createdByName ? ` by=@${task.createdByName}${departedCreator ? " creator=departed" : ""}` : "";
|
|
11672
|
-
const message = task.messageId ? ` msg=${task.messageId.slice(0, 8)}` : "";
|
|
11673
|
-
const legacy = task.isLegacy ? " legacy=read-only" : "";
|
|
11674
|
-
return `- ${channelRef} task #${taskNumber} [${status}]${creator}${message}${task.requiresResourceReceipt ? ` resource-receipt=${task.resourceReceiptRecordedAt ? "recorded" : "pending"}` : ""}${taskTimestamps(task)}${legacy} Current title: ${oneLineTaskTitle(task.title)}`.trimEnd();
|
|
11675
|
-
});
|
|
11676
|
-
return [`### ${status} (${rows.length})`, ...rendered].join("\n");
|
|
11677
|
-
})
|
|
11678
|
-
].join("\n");
|
|
11679
|
-
}
|
|
11680
|
-
/** Receipt for task create. */
|
|
11681
|
-
function formatAgentTasksCreated(channel, data) {
|
|
11682
|
-
const created = data.tasks.map((t) => {
|
|
11683
|
-
const assignee = t.claimedById ? t.claimedByName ? `@${t.claimedByName}` : "<unresolved>" : "unassigned";
|
|
11684
|
-
const resourceReceipt = t.requiresResourceReceipt ? " resource-receipt=pending" : "";
|
|
11685
|
-
return `#${t.taskNumber} [${t.status}] assignee=${assignee} claimedAt=${t.claimedAt ?? "null"} msg=${t.messageId.slice(0, 8)}${resourceReceipt} "${t.title}"`;
|
|
11686
|
-
}).join("\n");
|
|
11687
|
-
const threadHints = data.tasks.map((t) => `#${t.taskNumber} → raft message send --target "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
|
|
11688
|
-
const receipt = data.assignmentReceipt ? `\n\nAssignment receipt (msg=${data.assignmentReceipt.messageId.slice(0, 8)}):\n${data.assignmentReceipt.content}` : "";
|
|
11689
|
-
return `Created ${data.tasks.length} task(s) in ${channel}:\n${created}${receipt}\n\nTo follow up in each task's thread:\n${threadHints}`;
|
|
11690
|
-
}
|
|
11691
|
-
const BLOCKED_ACTION_COPY = { start_conflicting_execution: "starting conflicting implementation/change work" };
|
|
11692
|
-
function formatClaimConflict(label, conflict) {
|
|
11693
|
-
const holder = conflict.currentAssignee?.name ? `@${conflict.currentAssignee.name}` : "another actor";
|
|
11694
|
-
const blocked = conflict.blockedActions.map((action) => BLOCKED_ACTION_COPY[action] ?? action).join("; ");
|
|
11695
|
-
const examples = conflict.unblockedActionExamples.join(" · ");
|
|
11696
|
-
return [
|
|
11697
|
-
`${label}: Claim failed — ${holder} currently holds the implementation lock (assignment state as of ${conflict.observedAt}).`,
|
|
11698
|
-
` Blocked: ${blocked}.`,
|
|
11699
|
-
` Not blocked by this claim conflict (each still subject to its own authority/policy): ${examples}.`,
|
|
11700
|
-
` This is not a ruling on who owns or leads this lane. If you are its canonical owner or believe it is misrouted: correct the routing in the original thread, or file request_reassign (a request — it does not itself reassign).`
|
|
11701
|
-
].join("\n");
|
|
11702
|
-
}
|
|
11703
|
-
function canonicalAgentTaskTarget(target) {
|
|
11704
|
-
return target.replace(/^dm:user:/i, "dm:@");
|
|
11705
|
-
}
|
|
11706
|
-
function agentTaskThreadTarget(target, messageId) {
|
|
11707
|
-
return `${canonicalAgentTaskTarget(target)}:${messageId.slice(0, 8)}`;
|
|
11708
|
-
}
|
|
11709
|
-
/** Claim results incl. concurrency-lock guidance on failed claims. */
|
|
11710
|
-
function formatAgentClaimResults(channel, data) {
|
|
11711
|
-
const lines = data.results.map((r) => {
|
|
11712
|
-
const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
|
|
11713
|
-
if (r.success) return `${label} (msg:${r.messageId ? r.messageId.slice(0, 8) : ""}): claimed`;
|
|
11714
|
-
if (r.reason === "already claimed by you") return `${label}: already claimed by you.`;
|
|
11715
|
-
if (r.conflict?.kind === "claim_conflict") return formatClaimConflict(label, r.conflict);
|
|
11716
|
-
return `${label}: FAILED — ${r.reason || "already claimed"}. Do not start conflicting execution on this task or take over its scope without a redirect; a failed claim is a concurrency lock, not a ruling on lane ownership.`;
|
|
11717
|
-
});
|
|
11718
|
-
const succeeded = data.results.filter((r) => r.success).length;
|
|
11719
|
-
const failed = data.results.length - succeeded;
|
|
11720
|
-
let summary = `${succeeded} claimed`;
|
|
11721
|
-
if (failed > 0) summary += `, ${failed} failed`;
|
|
11722
|
-
const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} → raft message send --target "${agentTaskThreadTarget(channel, r.messageId)}"`).join("\n");
|
|
11723
|
-
const threadHint = claimedMsgs ? `\n\nFollow up in each task's thread:\n${claimedMsgs}` : "";
|
|
11724
|
-
return `Claim results (${summary}):\n${lines.join("\n")}${threadHint}`;
|
|
11725
|
-
}
|
|
11726
|
-
function formatAgentTaskUnclaimed(taskNumber) {
|
|
11727
|
-
return `#${taskNumber} unclaimed — now open.`;
|
|
11728
|
-
}
|
|
11729
|
-
function formatAgentTaskAssigned(taskNumber, assignee) {
|
|
11730
|
-
return assignee ? `#${taskNumber} assigned to ${assignee}.` : `#${taskNumber} unassigned — now open.`;
|
|
11731
|
-
}
|
|
11732
|
-
function formatAgentTaskStatusUpdated(taskNumber, status) {
|
|
11733
|
-
return `#${taskNumber} moved to ${status}.`;
|
|
11734
|
-
}
|
|
11735
|
-
function formatAgentTaskDeleted(taskNumber) {
|
|
11736
|
-
return `#${taskNumber} deleted.`;
|
|
11737
|
-
}
|
|
11738
|
-
/** Message→task conversion receipt. */
|
|
11739
|
-
function formatAgentTaskConverted(channel, task) {
|
|
11740
|
-
const target = `${canonicalAgentTaskTarget(channel)}:${task.messageId.slice(0, 8)}`;
|
|
11741
|
-
return [
|
|
11742
|
-
`Converted msg=${task.messageId.slice(0, 8)} to task #${task.taskNumber} [${task.status}] assignee=unassigned "${task.title}"`,
|
|
11743
|
-
"",
|
|
11744
|
-
`To follow up in the task's thread:`,
|
|
11745
|
-
`raft message send --target "${target}"`
|
|
11746
|
-
].join("\n");
|
|
11747
|
-
}
|
|
11748
|
-
function formatAgentTaskAmended(data) {
|
|
11749
|
-
const details = data.task.description === null ? "details: <none>" : `details:\n${data.task.description.split("\n").map((line) => ` ${line}`).join("\n")}`;
|
|
11750
|
-
return [
|
|
11751
|
-
`#${data.task.taskNumber} amended — revision ${data.task.revision}, event seq ${data.event.seq}.`,
|
|
11752
|
-
`title: ${data.task.title}`,
|
|
11753
|
-
details
|
|
11754
|
-
].join("\n");
|
|
11755
|
-
}
|
|
11756
|
-
function formatAgentTaskHistory(data) {
|
|
11757
|
-
const description = data.task.description === null ? "(none set)" : data.task.description.replace(/\n/g, `\n${" ".repeat(21)}`);
|
|
11758
|
-
const header = [
|
|
11759
|
-
`## Task #${data.task.taskNumber} history — revision ${data.task.revision}`,
|
|
11760
|
-
"",
|
|
11761
|
-
`Current title: ${data.task.title.replace(/\s+/g, " ").trim()}`,
|
|
11762
|
-
`Current description: ${description}`
|
|
11763
|
-
].join("\n");
|
|
11764
|
-
if (data.events.length === 0) return `${header}\n\nNo recorded events.`;
|
|
11765
|
-
return `${header}\n\n${data.events.map((event) => {
|
|
11766
|
-
const actor = event.actorType === "system" ? "@system" : event.actorName ? `@${event.actorName}` : "<unresolved>";
|
|
11767
|
-
return `seq=${event.seq} time=${event.createdAt} actor=${actor} type=${event.eventType}\n ${JSON.stringify(event.payload)}`;
|
|
11768
|
-
}).join("\n")}`;
|
|
11495
|
+
/** Reply where a received message came from. */
|
|
11496
|
+
function replyTo(client, message, request, frontier) {
|
|
11497
|
+
return sendMessage(client, {
|
|
11498
|
+
...request,
|
|
11499
|
+
target: message.target
|
|
11500
|
+
}, frontier);
|
|
11769
11501
|
}
|
|
11770
11502
|
//#endregion
|
|
11771
11503
|
//#region ../shared/src/agentOps/tasks.ts
|
|
@@ -11779,12 +11511,12 @@ function rowState(result) {
|
|
|
11779
11511
|
function claimNext(claim) {
|
|
11780
11512
|
const first = claim.rows.find((row) => row.mayWork);
|
|
11781
11513
|
if (first) {
|
|
11782
|
-
const
|
|
11514
|
+
const suffix = first.messageId ? first.messageId.slice(0, 8) : null;
|
|
11783
11515
|
return {
|
|
11784
11516
|
kind: "start_work",
|
|
11785
|
-
...
|
|
11786
|
-
command: `raft message send --target "${
|
|
11787
|
-
args: { thread }
|
|
11517
|
+
...suffix ? {
|
|
11518
|
+
command: `raft message send --target "${claim.target}:${suffix}"`,
|
|
11519
|
+
args: { thread: `${claim.target}:${suffix}` }
|
|
11788
11520
|
} : {},
|
|
11789
11521
|
why: "The claim is yours; post progress in the task's thread."
|
|
11790
11522
|
};
|
|
@@ -11795,7 +11527,20 @@ function claimNext(claim) {
|
|
|
11795
11527
|
};
|
|
11796
11528
|
}
|
|
11797
11529
|
function claimText(claim) {
|
|
11798
|
-
|
|
11530
|
+
const lines = claim.rows.map((row) => {
|
|
11531
|
+
const label = row.taskNumber ? `#${row.taskNumber}` : `msg:${row.ref}`;
|
|
11532
|
+
if (row.state === "claimed") return `${label}: claimed`;
|
|
11533
|
+
if (row.state === "already_yours") return `${label}: already claimed by you`;
|
|
11534
|
+
if (row.state === "conflict") return `${label}: held by ${row.holder?.name ? `@${row.holder.name}` : "another assignee"}`;
|
|
11535
|
+
return `${label}: refused${row.reason ? ` (${row.reason})` : ""}`;
|
|
11536
|
+
});
|
|
11537
|
+
const head = `Claim results (${claim.rows.filter((row) => row.mayWork).length} of ${claim.rows.length} authorise work):`;
|
|
11538
|
+
const next = claimNext(claim);
|
|
11539
|
+
return [
|
|
11540
|
+
head,
|
|
11541
|
+
...lines,
|
|
11542
|
+
next?.command ? `Follow up in the task's thread: ${next.command}` : next?.why ?? ""
|
|
11543
|
+
].filter(Boolean).join("\n");
|
|
11799
11544
|
}
|
|
11800
11545
|
async function claimTasks(client, request) {
|
|
11801
11546
|
const numbers = (request.taskNumbers ?? []).filter((n) => Number.isInteger(n) && n > 0);
|
|
@@ -11810,10 +11555,8 @@ async function claimTasks(client, request) {
|
|
|
11810
11555
|
if (!result.ok) return failureFromClientResult(result);
|
|
11811
11556
|
const data = result.data;
|
|
11812
11557
|
if (isHeldResponse(data)) {
|
|
11813
|
-
const { idempotencyKey: _unused, continuation: _c, ...heldBase } = projectHeld(request.target, data, "");
|
|
11814
11558
|
const held = {
|
|
11815
|
-
...
|
|
11816
|
-
request,
|
|
11559
|
+
...projectHeld(request.target, data),
|
|
11817
11560
|
retry: () => claimTasks(client, request)
|
|
11818
11561
|
};
|
|
11819
11562
|
return {
|
|
@@ -11821,10 +11564,13 @@ async function claimTasks(client, request) {
|
|
|
11821
11564
|
state: "held",
|
|
11822
11565
|
data: held,
|
|
11823
11566
|
next: {
|
|
11824
|
-
kind: "
|
|
11567
|
+
kind: "review_then_retry",
|
|
11825
11568
|
command: `raft message read --target "${request.target}"`,
|
|
11826
|
-
args: {
|
|
11827
|
-
|
|
11569
|
+
args: {
|
|
11570
|
+
target: request.target,
|
|
11571
|
+
newMessageCount: held.newMessageCount
|
|
11572
|
+
},
|
|
11573
|
+
why: "Unread messages in this channel may change the task; read them, then claim again if it is still right."
|
|
11828
11574
|
},
|
|
11829
11575
|
text: `Held — ${held.newMessageCount} unread ${held.newMessageCount === 1 ? "message" : "messages"} in ${request.target}. Your task claim was not applied.\nRead them with: raft message read --target "${request.target}"\nAfter reviewing, rerun the claim if it is still correct.`
|
|
11830
11576
|
};
|
|
@@ -11839,8 +11585,7 @@ async function claimTasks(client, request) {
|
|
|
11839
11585
|
mayWork: state === "claimed" || state === "already_yours",
|
|
11840
11586
|
reason: row.reason ?? null,
|
|
11841
11587
|
holder: row.conflict?.currentAssignee ?? null,
|
|
11842
|
-
conflict: row.conflict ?? null
|
|
11843
|
-
raw: row
|
|
11588
|
+
conflict: row.conflict ?? null
|
|
11844
11589
|
};
|
|
11845
11590
|
});
|
|
11846
11591
|
const claim = {
|
|
@@ -11856,235 +11601,6 @@ async function claimTasks(client, request) {
|
|
|
11856
11601
|
text: claimText(claim)
|
|
11857
11602
|
};
|
|
11858
11603
|
}
|
|
11859
|
-
async function listTasks(client, request) {
|
|
11860
|
-
const mine = request.mine === true;
|
|
11861
|
-
if (mine === Boolean(request.target?.trim())) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass exactly one of a channel target or mine: true." }));
|
|
11862
|
-
const result = await client.tasks.list({
|
|
11863
|
-
...mine ? { mine: "true" } : { channel: request.target },
|
|
11864
|
-
...request.status ? { status: request.status } : {}
|
|
11865
|
-
});
|
|
11866
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
11867
|
-
const data = result.data;
|
|
11868
|
-
const board = {
|
|
11869
|
-
scope: mine ? "mine" : "channel",
|
|
11870
|
-
target: mine ? null : request.target,
|
|
11871
|
-
tasks: data.tasks,
|
|
11872
|
-
coverage: data.coverage ?? null
|
|
11873
|
-
};
|
|
11874
|
-
const text = mine ? formatAgentMyTaskList(data, request.status) : formatAgentTaskList(request.target, data, request.status);
|
|
11875
|
-
const open = board.tasks.find((t) => t.status === "todo" && !t.claimedByName);
|
|
11876
|
-
const next = open?.taskNumber && board.target ? {
|
|
11877
|
-
kind: "claim_task",
|
|
11878
|
-
command: `raft task claim --target "${board.target}" --number ${open.taskNumber}`,
|
|
11879
|
-
args: {
|
|
11880
|
-
target: board.target,
|
|
11881
|
-
taskNumbers: [open.taskNumber]
|
|
11882
|
-
},
|
|
11883
|
-
why: "An unassigned todo task is open; claim it before working on it."
|
|
11884
|
-
} : null;
|
|
11885
|
-
return {
|
|
11886
|
-
ok: true,
|
|
11887
|
-
state: board.tasks.length > 0 ? "board" : "empty",
|
|
11888
|
-
data: board,
|
|
11889
|
-
next,
|
|
11890
|
-
text
|
|
11891
|
-
};
|
|
11892
|
-
}
|
|
11893
|
-
async function createTasks(client, request) {
|
|
11894
|
-
if (!request.target?.trim() || !request.tasks?.length) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and at least one task title are required." }));
|
|
11895
|
-
const result = await client.tasks.create({
|
|
11896
|
-
channel: request.target,
|
|
11897
|
-
tasks: request.tasks.map((t) => ({
|
|
11898
|
-
title: t.title,
|
|
11899
|
-
...t.createsResource ? { creates_resource: true } : {}
|
|
11900
|
-
})),
|
|
11901
|
-
...request.assignee ? { assignee: request.assignee } : {}
|
|
11902
|
-
});
|
|
11903
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
11904
|
-
const data = result.data;
|
|
11905
|
-
const first = data.tasks[0];
|
|
11906
|
-
return {
|
|
11907
|
-
ok: true,
|
|
11908
|
-
state: "created",
|
|
11909
|
-
data: {
|
|
11910
|
-
...data,
|
|
11911
|
-
target: request.target
|
|
11912
|
-
},
|
|
11913
|
-
next: first ? {
|
|
11914
|
-
kind: "post_in_task_thread",
|
|
11915
|
-
command: `raft message send --target "${agentTaskThreadTarget(request.target, first.messageId)}"`,
|
|
11916
|
-
args: { thread: agentTaskThreadTarget(request.target, first.messageId) },
|
|
11917
|
-
why: "Follow up in each task's thread."
|
|
11918
|
-
} : null,
|
|
11919
|
-
text: formatAgentTasksCreated(request.target, data)
|
|
11920
|
-
};
|
|
11921
|
-
}
|
|
11922
|
-
function requireTaskRef(request) {
|
|
11923
|
-
if (!request.target?.trim() || !Number.isInteger(request.taskNumber) || request.taskNumber <= 0) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and a positive task number are required." }));
|
|
11924
|
-
return null;
|
|
11925
|
-
}
|
|
11926
|
-
async function unclaimTask(client, request) {
|
|
11927
|
-
const invalid = requireTaskRef(request);
|
|
11928
|
-
if (invalid) return invalid;
|
|
11929
|
-
const result = await client.tasks.unclaim({
|
|
11930
|
-
channel: request.target,
|
|
11931
|
-
task_number: request.taskNumber
|
|
11932
|
-
});
|
|
11933
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
11934
|
-
return {
|
|
11935
|
-
ok: true,
|
|
11936
|
-
state: "unclaimed",
|
|
11937
|
-
data: request,
|
|
11938
|
-
next: null,
|
|
11939
|
-
text: formatAgentTaskUnclaimed(request.taskNumber)
|
|
11940
|
-
};
|
|
11941
|
-
}
|
|
11942
|
-
async function assignTask(client, request) {
|
|
11943
|
-
const invalid = requireTaskRef(request);
|
|
11944
|
-
if (invalid) return invalid;
|
|
11945
|
-
const result = await client.tasks.assign({
|
|
11946
|
-
channel: request.target,
|
|
11947
|
-
task_number: request.taskNumber,
|
|
11948
|
-
assignee: request.assignee,
|
|
11949
|
-
...request.expectedRevision === void 0 ? {} : { expected_revision: request.expectedRevision }
|
|
11950
|
-
});
|
|
11951
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
11952
|
-
const assignee = result.data.assignee;
|
|
11953
|
-
return {
|
|
11954
|
-
ok: true,
|
|
11955
|
-
state: assignee ? "assigned" : "unassigned",
|
|
11956
|
-
data: {
|
|
11957
|
-
target: request.target,
|
|
11958
|
-
taskNumber: request.taskNumber,
|
|
11959
|
-
assignee,
|
|
11960
|
-
revision: result.data.revision
|
|
11961
|
-
},
|
|
11962
|
-
next: null,
|
|
11963
|
-
text: formatAgentTaskAssigned(request.taskNumber, assignee)
|
|
11964
|
-
};
|
|
11965
|
-
}
|
|
11966
|
-
function heldTaskWrite(request, data, action, kind) {
|
|
11967
|
-
const { idempotencyKey: _k, continuation: _c, ...base } = projectHeld(request.target, data, "");
|
|
11968
|
-
const held = {
|
|
11969
|
-
...base,
|
|
11970
|
-
request
|
|
11971
|
-
};
|
|
11972
|
-
return {
|
|
11973
|
-
ok: true,
|
|
11974
|
-
state: "held",
|
|
11975
|
-
data: held,
|
|
11976
|
-
next: {
|
|
11977
|
-
kind,
|
|
11978
|
-
command: `raft message read --target "${request.target}"`,
|
|
11979
|
-
args: { ...request },
|
|
11980
|
-
why: `Unread messages in this channel may change the task; read them (frontier.recordHeld(held) once the model saw them), then repeat with these args if it is still right.`
|
|
11981
|
-
},
|
|
11982
|
-
text: `Held — ${held.newMessageCount} unread ${held.newMessageCount === 1 ? "message" : "messages"} in ${request.target}. ${action}\nRead them with: raft message read --target "${request.target}"\nAfter reviewing, repeat the operation if it is still correct.`
|
|
11983
|
-
};
|
|
11984
|
-
}
|
|
11985
|
-
async function updateTaskStatus(client, request) {
|
|
11986
|
-
const invalid = requireTaskRef(request);
|
|
11987
|
-
if (invalid) return invalid;
|
|
11988
|
-
const result = await client.tasks.updateStatus({
|
|
11989
|
-
channel: request.target,
|
|
11990
|
-
task_number: request.taskNumber,
|
|
11991
|
-
status: request.status
|
|
11992
|
-
});
|
|
11993
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
11994
|
-
if (isHeldResponse(result.data)) return heldTaskWrite(request, result.data, "The status change was not applied.", "retry_update_status");
|
|
11995
|
-
return {
|
|
11996
|
-
ok: true,
|
|
11997
|
-
state: "updated",
|
|
11998
|
-
data: request,
|
|
11999
|
-
next: request.status === "in_review" ? {
|
|
12000
|
-
kind: "await_review",
|
|
12001
|
-
why: "A human validates the work; set done after approval."
|
|
12002
|
-
} : null,
|
|
12003
|
-
text: formatAgentTaskStatusUpdated(request.taskNumber, request.status)
|
|
12004
|
-
};
|
|
12005
|
-
}
|
|
12006
|
-
async function amendTask(client, request) {
|
|
12007
|
-
const invalid = requireTaskRef(request);
|
|
12008
|
-
if (invalid) return invalid;
|
|
12009
|
-
if (request.title === void 0 && request.description === void 0) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass a new title, a new description, or description: null to clear it." }));
|
|
12010
|
-
const result = await client.tasks.amend({
|
|
12011
|
-
channel: request.target,
|
|
12012
|
-
task_number: request.taskNumber,
|
|
12013
|
-
...request.title === void 0 ? {} : { title: request.title },
|
|
12014
|
-
...request.description === void 0 ? {} : { description: request.description }
|
|
12015
|
-
});
|
|
12016
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12017
|
-
if (isHeldResponse(result.data)) return heldTaskWrite(request, result.data, "The amendment was not applied.", "retry_amend");
|
|
12018
|
-
return {
|
|
12019
|
-
ok: true,
|
|
12020
|
-
state: "amended",
|
|
12021
|
-
data: {
|
|
12022
|
-
...result.data,
|
|
12023
|
-
target: request.target
|
|
12024
|
-
},
|
|
12025
|
-
next: null,
|
|
12026
|
-
text: formatAgentTaskAmended(result.data)
|
|
12027
|
-
};
|
|
12028
|
-
}
|
|
12029
|
-
async function taskHistory(client, request) {
|
|
12030
|
-
const invalid = requireTaskRef(request);
|
|
12031
|
-
if (invalid) return invalid;
|
|
12032
|
-
const result = await client.tasks.history({
|
|
12033
|
-
channel: request.target,
|
|
12034
|
-
task_number: request.taskNumber
|
|
12035
|
-
});
|
|
12036
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12037
|
-
return {
|
|
12038
|
-
ok: true,
|
|
12039
|
-
state: "history",
|
|
12040
|
-
data: {
|
|
12041
|
-
...result.data,
|
|
12042
|
-
target: request.target
|
|
12043
|
-
},
|
|
12044
|
-
next: null,
|
|
12045
|
-
text: formatAgentTaskHistory(result.data)
|
|
12046
|
-
};
|
|
12047
|
-
}
|
|
12048
|
-
async function convertMessageToTask(client, request) {
|
|
12049
|
-
if (!request.target?.trim() || !request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and a message id are required." }));
|
|
12050
|
-
const result = await client.tasks.convert({
|
|
12051
|
-
channel: request.target,
|
|
12052
|
-
message_id: request.messageId
|
|
12053
|
-
});
|
|
12054
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12055
|
-
const task = result.data.task;
|
|
12056
|
-
return {
|
|
12057
|
-
ok: true,
|
|
12058
|
-
state: "converted",
|
|
12059
|
-
data: {
|
|
12060
|
-
target: request.target,
|
|
12061
|
-
task
|
|
12062
|
-
},
|
|
12063
|
-
next: {
|
|
12064
|
-
kind: "post_in_task_thread",
|
|
12065
|
-
command: `raft message send --target "${agentTaskThreadTarget(request.target, task.messageId)}"`,
|
|
12066
|
-
args: { thread: agentTaskThreadTarget(request.target, task.messageId) },
|
|
12067
|
-
why: "Follow up in the task's thread; it is unassigned until someone claims it."
|
|
12068
|
-
},
|
|
12069
|
-
text: formatAgentTaskConverted(request.target, task)
|
|
12070
|
-
};
|
|
12071
|
-
}
|
|
12072
|
-
async function deleteTask(client, request) {
|
|
12073
|
-
const invalid = requireTaskRef(request);
|
|
12074
|
-
if (invalid) return invalid;
|
|
12075
|
-
const result = await client.tasks.delete({
|
|
12076
|
-
channel: request.target,
|
|
12077
|
-
task_number: request.taskNumber
|
|
12078
|
-
});
|
|
12079
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12080
|
-
return {
|
|
12081
|
-
ok: true,
|
|
12082
|
-
state: "deleted",
|
|
12083
|
-
data: request,
|
|
12084
|
-
next: null,
|
|
12085
|
-
text: formatAgentTaskDeleted(request.taskNumber)
|
|
12086
|
-
};
|
|
12087
|
-
}
|
|
12088
11604
|
//#endregion
|
|
12089
11605
|
//#region ../shared/src/agentOps/wake.ts
|
|
12090
11606
|
const RAFT_INBOX_NOTICE_SCHEMA = "raft-agent-inbox-notice.v1";
|
|
@@ -12252,1859 +11768,53 @@ async function unregisterWebhook(client) {
|
|
|
12252
11768
|
};
|
|
12253
11769
|
}
|
|
12254
11770
|
//#endregion
|
|
12255
|
-
//#region
|
|
12256
|
-
function
|
|
12257
|
-
|
|
12258
|
-
const
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
}
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
}
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
if (member.serverRole) details.push(`server role=${member.serverRole}`);
|
|
12281
|
-
if (member.channelRole) details.push(`channel role=${member.channelRole}`);
|
|
12282
|
-
if (member.channelAdminBasis) details.push(`admin via=${member.channelAdminBasis}`);
|
|
12283
|
-
return details.length > 0 ? ` [${details.join(", ")}]` : "";
|
|
12284
|
-
}
|
|
12285
|
-
function agentStatusLabel(agent) {
|
|
12286
|
-
const lifecycle = agent.status?.trim() || "unknown";
|
|
12287
|
-
const activity = agent.activity?.trim();
|
|
12288
|
-
if (!activity) return lifecycle;
|
|
12289
|
-
const activityWithDetail = agent.activityDetail?.trim() ? `${activity}: ${agent.activityDetail.trim()}` : activity;
|
|
12290
|
-
return activity === lifecycle ? lifecycle : `${lifecycle}; ${activityWithDetail}`;
|
|
12291
|
-
}
|
|
12292
|
-
function formatCurrentAgent(data) {
|
|
12293
|
-
if (!data.serverRole) return "";
|
|
12294
|
-
const lines = ["### Current Agent"];
|
|
12295
|
-
lines.push(`- Role: ${data.serverRole}`);
|
|
12296
|
-
return `${lines.join("\n")}\n\n`;
|
|
12297
|
-
}
|
|
12298
|
-
/** Server overview: runtime context, channels, agents, humans. */
|
|
12299
|
-
function formatAgentServerInfo(data) {
|
|
12300
|
-
let text = "## Server\n\n";
|
|
12301
|
-
const channels = data.channels ?? [];
|
|
12302
|
-
const agents = data.agents ?? [];
|
|
12303
|
-
const humans = data.humans ?? [];
|
|
12304
|
-
text += formatRuntimeContext(data.runtimeContext);
|
|
12305
|
-
text += formatCurrentAgent(data);
|
|
12306
|
-
text += "### Channels\n";
|
|
12307
|
-
text += "Visible public channels may appear even when `joined=false`. Private channels are shown only when you are a member; do not disclose private-channel names, membership, or content outside that channel. Use channel attention commands (`raft channel join`, `leave`, `mute`, `unmute`; `raft thread unfollow`) for your own delivery state. Existing channel management commands (`raft channel create`, `update`, `archive`, `unarchive`, `add-member`, `remove-member`) are authorized per channel; a channel-admin role never grants delete, visibility, federation, or server-profile actions. There is no Agent command for changing channel roles. Run any subcommand with `--help` for syntax.\n";
|
|
12308
|
-
text += "Server-profile changes still use raft server update and remain server-role gated.\n";
|
|
12309
|
-
text += "Mute state is shown when the server provides it; otherwise it is omitted.\n";
|
|
12310
|
-
if (channels.length > 0) for (const t of channels) {
|
|
12311
|
-
const statusParts = [t.type === "private" || t.type === "dm" ? "private" : "public", t.joined ? "joined" : "not joined"];
|
|
12312
|
-
if (t.channelRole) statusParts.push(`channel role=${t.channelRole}`);
|
|
12313
|
-
if (t.channelAdminBasis) statusParts.push(`admin via=${t.channelAdminBasis}`);
|
|
12314
|
-
const muted = channelMuted(t);
|
|
12315
|
-
if (muted !== void 0) statusParts.push(muted ? "muted" : "not muted");
|
|
12316
|
-
const status = statusParts.join(", ");
|
|
12317
|
-
const ref = agentChannelRef(t.name, t.type);
|
|
12318
|
-
text += t.description ? ` - ${ref} [${status}] — ${t.description}\n` : ` - ${ref} [${status}]\n`;
|
|
12319
|
-
}
|
|
12320
|
-
else text += " (none)\n";
|
|
12321
|
-
text += "\n### Agents\n";
|
|
12322
|
-
text += "Other AI agents in this server.\n";
|
|
12323
|
-
text += "Role labels show server-level owner/admin authority; no role label means ordinary member.\n";
|
|
12324
|
-
if (agents.length > 0) for (const a of agents) {
|
|
12325
|
-
const role = roleLabel(a.role);
|
|
12326
|
-
const status = agentStatusLabel(a);
|
|
12327
|
-
text += a.description ? ` - @${a.name} (${status})${role} — ${a.description}\n` : ` - @${a.name} (${status})${role}\n`;
|
|
12328
|
-
}
|
|
12329
|
-
else text += " (none)\n";
|
|
12330
|
-
text += "\n### Humans\n";
|
|
12331
|
-
text += "To start a new DM: raft message send --target \"dm:@name\" <<'RAFTMSG' followed by the message body and RAFTMSG. To reply in an existing DM: reuse the target from received messages.\n";
|
|
12332
|
-
text += "Role labels show server-level owner/admin authority; no role label means ordinary member.\n";
|
|
12333
|
-
if (humans.length > 0) for (const u of humans) {
|
|
12334
|
-
const role = roleLabel(u.role);
|
|
12335
|
-
text += u.description ? ` - @${u.name}${role} — ${u.description}\n` : ` - @${u.name}${role}\n`;
|
|
12336
|
-
}
|
|
12337
|
-
else text += " (none)\n";
|
|
12338
|
-
return text;
|
|
12339
|
-
}
|
|
12340
|
-
function agentChannelRef(name, type) {
|
|
12341
|
-
if (type === "dm") return `dm:@${name}`;
|
|
12342
|
-
return name.startsWith("#") ? name : `#${name}`;
|
|
12343
|
-
}
|
|
12344
|
-
function channelVisibility(channel) {
|
|
12345
|
-
const type = channel.type?.trim();
|
|
12346
|
-
return type === "private" || type === "dm" ? "private" : "public";
|
|
12347
|
-
}
|
|
12348
|
-
function channelMuted(channel) {
|
|
12349
|
-
if (typeof channel.muted === "boolean") return channel.muted;
|
|
12350
|
-
if (typeof channel.activityMuted === "boolean") return channel.activityMuted;
|
|
12351
|
-
}
|
|
12352
|
-
function channelStatus(channel) {
|
|
12353
|
-
const parts = [channelVisibility(channel), channel.joined ? "joined" : "not joined"];
|
|
12354
|
-
const muted = channelMuted(channel);
|
|
12355
|
-
if (channel.channelRole) parts.push(`channel role=${channel.channelRole}`);
|
|
12356
|
-
if (channel.channelAdminBasis) parts.push(`admin via=${channel.channelAdminBasis}`);
|
|
12357
|
-
if (muted !== void 0) parts.push(muted ? "muted" : "not muted");
|
|
12358
|
-
if (typeof channel.archived === "boolean") parts.push(channel.archived ? "archived" : "not archived");
|
|
12359
|
-
return parts.join(", ");
|
|
12360
|
-
}
|
|
12361
|
-
function formatPageFooter(page) {
|
|
12362
|
-
if (!page) return "";
|
|
12363
|
-
const start = page.total === 0 ? 0 : Math.min(page.offset + 1, page.total);
|
|
12364
|
-
const end = Math.min(page.offset + page.limit, page.total);
|
|
12365
|
-
const lines = [`\nShowing ${start}-${end} of ${page.total}.`];
|
|
12366
|
-
if (page.nextCommand && end < page.total) lines.push(`More: ${page.nextCommand}`);
|
|
12367
|
-
return `${lines.join("\n")}\n`;
|
|
12368
|
-
}
|
|
12369
|
-
/** Compact server summary. */
|
|
12370
|
-
function formatAgentServerSummary(data) {
|
|
12371
|
-
const channels = data.channels ?? [];
|
|
12372
|
-
const agents = data.agents ?? [];
|
|
12373
|
-
const humans = data.humans ?? [];
|
|
12374
|
-
const joined = channels.filter((channel) => channel.joined).length;
|
|
12375
|
-
return `${[
|
|
12376
|
-
"## Server",
|
|
12377
|
-
"",
|
|
12378
|
-
`Channels: ${channels.length} visible (${joined} joined)`,
|
|
12379
|
-
`Agents: ${agents.length}`,
|
|
12380
|
-
`Humans: ${humans.length}`,
|
|
12381
|
-
"",
|
|
12382
|
-
"Narrow queries:",
|
|
12383
|
-
"- raft server info --channels",
|
|
12384
|
-
"- raft server info --agents",
|
|
12385
|
-
"- raft server info --humans",
|
|
12386
|
-
"- raft channel info <name>",
|
|
12387
|
-
"- raft user info <name>",
|
|
12388
|
-
"",
|
|
12389
|
-
"Full dump: raft server info --full"
|
|
12390
|
-
].join("\n")}\n`;
|
|
12391
|
-
}
|
|
12392
|
-
/** Channel listing page. */
|
|
12393
|
-
function formatAgentServerChannels(channels, page) {
|
|
12394
|
-
const lines = [
|
|
12395
|
-
"## Server Channels",
|
|
12396
|
-
"",
|
|
12397
|
-
"Private channels are shown only when this agent is a member. Do not disclose private-channel names or metadata outside that channel."
|
|
12398
|
-
];
|
|
12399
|
-
if (channels.length === 0) lines.push("(none)");
|
|
12400
|
-
else for (const channel of channels) {
|
|
12401
|
-
const description = channel.description?.trim();
|
|
12402
|
-
lines.push(description ? `${agentChannelRef(channel.name, channel.type)} [${channelStatus(channel)}] — ${description}` : `${agentChannelRef(channel.name, channel.type)} [${channelStatus(channel)}]`);
|
|
12403
|
-
}
|
|
12404
|
-
return `${lines.join("\n")}${formatPageFooter(page)}`;
|
|
12405
|
-
}
|
|
12406
|
-
/** Agent listing page. */
|
|
12407
|
-
function formatAgentServerAgents(agents, page) {
|
|
12408
|
-
const lines = [
|
|
12409
|
-
"## Server Agents",
|
|
12410
|
-
"",
|
|
12411
|
-
"Role labels show server-level owner/admin authority; no role label means ordinary member."
|
|
12412
|
-
];
|
|
12413
|
-
if (agents.length === 0) lines.push("(none)");
|
|
12414
|
-
else for (const agent of agents) {
|
|
12415
|
-
const role = roleLabel(agent.role);
|
|
12416
|
-
const status = agentStatusLabel(agent);
|
|
12417
|
-
lines.push(agent.description ? `@${agent.name} (${status})${role} — ${agent.description}` : `@${agent.name} (${status})${role}`);
|
|
12418
|
-
}
|
|
12419
|
-
return `${lines.join("\n")}${formatPageFooter(page)}`;
|
|
12420
|
-
}
|
|
12421
|
-
/** Human listing page. */
|
|
12422
|
-
function formatAgentServerHumans(humans, page) {
|
|
12423
|
-
const lines = [
|
|
12424
|
-
"## Server Humans",
|
|
12425
|
-
"",
|
|
12426
|
-
"Role labels show server-level owner/admin authority; no role label means ordinary member."
|
|
12427
|
-
];
|
|
12428
|
-
if (humans.length === 0) lines.push("(none)");
|
|
12429
|
-
else for (const human of humans) {
|
|
12430
|
-
const role = roleLabel(human.role);
|
|
12431
|
-
lines.push(human.description ? `@${human.name}${role} — ${human.description}` : `@${human.name}${role}`);
|
|
12432
|
-
}
|
|
12433
|
-
return `${lines.join("\n")}${formatPageFooter(page)}`;
|
|
12434
|
-
}
|
|
12435
|
-
/** Channel membership with server-role labels. */
|
|
12436
|
-
function formatAgentChannelMembers(data) {
|
|
12437
|
-
let text = "## Channel Members\n\n";
|
|
12438
|
-
const ref = data.channel?.ref ?? "(unknown)";
|
|
12439
|
-
const type = data.channel?.type ? ` (${data.channel.type})` : "";
|
|
12440
|
-
const agents = data.agents ?? [];
|
|
12441
|
-
const humans = data.humans ?? [];
|
|
12442
|
-
text += `Channel: ${ref}${type}\n`;
|
|
12443
|
-
text += "Members means join/post authority for this surface.\n\n";
|
|
12444
|
-
text += "### Agents\n";
|
|
12445
|
-
text += "Server and stored channel roles are shown separately when available.\n";
|
|
12446
|
-
if (agents.length > 0) for (const a of agents) {
|
|
12447
|
-
const role = roleLabel(a.role);
|
|
12448
|
-
const channelRole = channelMemberRoleDetail(a);
|
|
12449
|
-
const status = agentStatusLabel(a);
|
|
12450
|
-
text += a.description ? ` - @${a.name} (${status})${role}${channelRole} — ${a.description}\n` : ` - @${a.name} (${status})${role}${channelRole}\n`;
|
|
12451
|
-
}
|
|
12452
|
-
else text += " (none)\n";
|
|
12453
|
-
text += "\n### Humans\n";
|
|
12454
|
-
text += "Server and stored channel roles are shown separately when available.\n";
|
|
12455
|
-
if (humans.length > 0) for (const u of humans) {
|
|
12456
|
-
const role = roleLabel(u.role);
|
|
12457
|
-
const channelRole = channelMemberRoleDetail(u);
|
|
12458
|
-
text += u.description ? ` - @${u.name}${role}${channelRole} — ${u.description}\n` : ` - @${u.name}${role}${channelRole}\n`;
|
|
12459
|
-
}
|
|
12460
|
-
else text += " (none)\n";
|
|
12461
|
-
return text;
|
|
12462
|
-
}
|
|
12463
|
-
//#endregion
|
|
12464
|
-
//#region ../shared/src/runtimeCatalog.ts
|
|
12465
|
-
const EXTERNAL_AGENT_RUNTIME_ID = "external";
|
|
12466
|
-
const EXTERNAL_AGENT_RUNTIME_DISPLAY_NAME = "External agent";
|
|
12467
|
-
function isExternalAgentRuntime(runtime) {
|
|
12468
|
-
return runtime === EXTERNAL_AGENT_RUNTIME_ID;
|
|
12469
|
-
}
|
|
12470
|
-
const RUNTIMES = [
|
|
12471
|
-
{
|
|
12472
|
-
id: "claude",
|
|
12473
|
-
displayName: "Claude Code",
|
|
12474
|
-
abbreviation: "CC",
|
|
12475
|
-
binary: "claude",
|
|
12476
|
-
supported: true
|
|
12477
|
-
},
|
|
12478
|
-
{
|
|
12479
|
-
id: "codex",
|
|
12480
|
-
displayName: "Codex CLI",
|
|
12481
|
-
abbreviation: "CX",
|
|
12482
|
-
binary: "codex",
|
|
12483
|
-
supported: true
|
|
12484
|
-
},
|
|
12485
|
-
{
|
|
12486
|
-
id: "grok",
|
|
12487
|
-
displayName: "Grok Build",
|
|
12488
|
-
abbreviation: "GK",
|
|
12489
|
-
binary: "grok",
|
|
12490
|
-
supported: true
|
|
12491
|
-
},
|
|
12492
|
-
{
|
|
12493
|
-
id: "builtin",
|
|
12494
|
-
displayName: "Built-in Pi",
|
|
12495
|
-
abbreviation: "BP",
|
|
12496
|
-
binary: "",
|
|
12497
|
-
supported: true
|
|
12498
|
-
},
|
|
12499
|
-
{
|
|
12500
|
-
id: "antigravity",
|
|
12501
|
-
displayName: "Antigravity CLI",
|
|
12502
|
-
abbreviation: "AG",
|
|
12503
|
-
binary: "agy",
|
|
12504
|
-
supported: true,
|
|
12505
|
-
deprecated: true
|
|
12506
|
-
},
|
|
12507
|
-
{
|
|
12508
|
-
id: "kimi-sdk",
|
|
12509
|
-
displayName: "Kimi Code",
|
|
12510
|
-
abbreviation: "KC",
|
|
12511
|
-
binary: "",
|
|
12512
|
-
supported: true
|
|
12513
|
-
},
|
|
12514
|
-
{
|
|
12515
|
-
id: "kimi",
|
|
12516
|
-
displayName: "Kimi CLI",
|
|
12517
|
-
abbreviation: "KL",
|
|
12518
|
-
binary: "kimi",
|
|
12519
|
-
supported: true,
|
|
12520
|
-
deprecated: true
|
|
12521
|
-
},
|
|
12522
|
-
{
|
|
12523
|
-
id: "copilot",
|
|
12524
|
-
displayName: "Copilot CLI",
|
|
12525
|
-
abbreviation: "CP",
|
|
12526
|
-
binary: "copilot",
|
|
12527
|
-
supported: true
|
|
12528
|
-
},
|
|
12529
|
-
{
|
|
12530
|
-
id: "cursor",
|
|
12531
|
-
displayName: "Cursor CLI",
|
|
12532
|
-
abbreviation: "CU",
|
|
12533
|
-
binary: "cursor-agent",
|
|
12534
|
-
supported: true
|
|
12535
|
-
},
|
|
12536
|
-
{
|
|
12537
|
-
id: "gemini",
|
|
12538
|
-
displayName: "Gemini CLI",
|
|
12539
|
-
abbreviation: "GM",
|
|
12540
|
-
binary: "gemini",
|
|
12541
|
-
supported: true,
|
|
12542
|
-
deprecated: true
|
|
12543
|
-
},
|
|
12544
|
-
{
|
|
12545
|
-
id: "opencode",
|
|
12546
|
-
displayName: "OpenCode",
|
|
12547
|
-
abbreviation: "OC",
|
|
12548
|
-
binary: "opencode",
|
|
12549
|
-
supported: true
|
|
12550
|
-
},
|
|
12551
|
-
{
|
|
12552
|
-
id: "pi",
|
|
12553
|
-
displayName: "Pi",
|
|
12554
|
-
abbreviation: "PI",
|
|
12555
|
-
binary: "pi",
|
|
12556
|
-
supported: true
|
|
12557
|
-
}
|
|
12558
|
-
];
|
|
12559
|
-
function isRuntimeDeprecated(runtimeId) {
|
|
12560
|
-
return RUNTIMES.some((runtime) => runtime.id === runtimeId && Boolean(runtime.deprecated));
|
|
12561
|
-
}
|
|
12562
|
-
/** Map runtime ID → display name. Falls back to the ID itself. */
|
|
12563
|
-
function getRuntimeDisplayName(id) {
|
|
12564
|
-
if (isExternalAgentRuntime(id)) return EXTERNAL_AGENT_RUNTIME_DISPLAY_NAME;
|
|
12565
|
-
return RUNTIMES.find((r) => r.id === id)?.displayName ?? id;
|
|
12566
|
-
}
|
|
12567
|
-
//#endregion
|
|
12568
|
-
//#region ../shared/src/agentText/threadsProfile.ts
|
|
12569
|
-
/**
|
|
12570
|
-
* Runtime label for identity display in agent-facing output: product name plus
|
|
12571
|
-
* lifecycle status. Agent-facing text is English-only, so the suffix is literal
|
|
12572
|
-
* here; the web surface renders the same status from the i18n catalog.
|
|
12573
|
-
*/
|
|
12574
|
-
function agentRuntimeLabelWithStatus(runtimeId) {
|
|
12575
|
-
const name = getRuntimeDisplayName(runtimeId);
|
|
12576
|
-
return isRuntimeDeprecated(runtimeId) ? `${name} (deprecated)` : name;
|
|
12577
|
-
}
|
|
12578
|
-
function formatDoneSuffix(thread) {
|
|
12579
|
-
return thread.doneAt ? `, doneAt=${thread.doneAt}` : "";
|
|
12580
|
-
}
|
|
12581
|
-
/** Followed thread list for the bound agent, including exact thread targets. */
|
|
12582
|
-
function formatAgentThreadList(threads) {
|
|
12583
|
-
if (threads.length === 0) return "No followed threads.";
|
|
12584
|
-
return [`Followed threads (${threads.length}):`, ...threads.map((thread) => `- ${thread.target} (${thread.threadChannelId}, parent=${thread.parentChannelRef}, followedAt=${thread.followedAt}, reason=${thread.reason}${formatDoneSuffix(thread)})`)].join("\n");
|
|
12585
|
-
}
|
|
12586
|
-
function formatCreatedAgents(createdAgents, runtimeLabel) {
|
|
12587
|
-
if (createdAgents.length === 0) return ["- Created Agents: none"];
|
|
12588
|
-
return [`- Created Agents (${createdAgents.length}):`, ...createdAgents.map((createdAgent) => ` - @${createdAgent.name} (${runtimeLabel(createdAgent.runtime)}, ${createdAgent.status})`)];
|
|
12589
|
-
}
|
|
12590
|
-
function formatHumanProfile(profile, runtimeLabel) {
|
|
12591
|
-
const lines = [
|
|
12592
|
-
"## Profile",
|
|
12593
|
-
"",
|
|
12594
|
-
"- Type: human",
|
|
12595
|
-
`- Handle: @${profile.name}`,
|
|
12596
|
-
`- Display Name: ${profile.displayName ?? "(none)"}`,
|
|
12597
|
-
`- Description: ${profile.description ?? "(none)"}`,
|
|
12598
|
-
`- Membership: ${profile.membershipStatus}`
|
|
12599
|
-
];
|
|
12600
|
-
if (profile.role) lines.push(`- Role: ${profile.role}`);
|
|
12601
|
-
if (profile.joinedAt) lines.push(`- Joined: ${profile.joinedAt}`);
|
|
12602
|
-
if (profile.email) lines.push(`- Email: ${profile.email}`);
|
|
12603
|
-
return [...lines, ...formatCreatedAgents(profile.createdAgents, runtimeLabel)].join("\n");
|
|
12604
|
-
}
|
|
12605
|
-
function formatCreator(profile) {
|
|
12606
|
-
if (!profile.creator) return null;
|
|
12607
|
-
return profile.creator.displayName ? `${profile.creator.displayName} (@${profile.creator.name})` : `@${profile.creator.name}`;
|
|
12608
|
-
}
|
|
12609
|
-
function formatAgentProfileCard(profile, runtimeLabel) {
|
|
12610
|
-
const lines = [
|
|
12611
|
-
"## Profile",
|
|
12612
|
-
"",
|
|
12613
|
-
"- Type: agent",
|
|
12614
|
-
`- Handle: @${profile.name}`,
|
|
12615
|
-
`- Display Name: ${profile.displayName ?? "(none)"}`,
|
|
12616
|
-
`- Description: ${profile.description ?? "(none)"}`,
|
|
12617
|
-
`- Status: ${profile.status}`,
|
|
12618
|
-
`- Role: ${profile.serverRole}`,
|
|
12619
|
-
`- Runtime: ${runtimeLabel(profile.runtime)}`,
|
|
12620
|
-
`- Model: ${profile.model}`,
|
|
12621
|
-
`- Reasoning: ${profile.reasoningEffort ?? "medium"}`
|
|
12622
|
-
];
|
|
12623
|
-
if (profile.executionMode) lines.push(`- Execution: ${profile.executionMode}`);
|
|
12624
|
-
if (profile.computerName || profile.computerId) {
|
|
12625
|
-
const label = profile.computerName && profile.computerId ? `${profile.computerName} (${profile.computerId})` : profile.computerName ?? profile.computerId;
|
|
12626
|
-
lines.push(`- Computer: ${label}`);
|
|
12627
|
-
}
|
|
12628
|
-
if (profile.computerHostname) lines.push(`- Hostname: ${profile.computerHostname}`);
|
|
12629
|
-
if (profile.daemonVersion) lines.push(`- Daemon: v${profile.daemonVersion}`);
|
|
12630
|
-
lines.push(`- Created: ${profile.createdAt}`);
|
|
12631
|
-
if (profile.deletedAt) lines.push(`- Deleted At: ${profile.deletedAt}`);
|
|
12632
|
-
const creator = formatCreator(profile);
|
|
12633
|
-
if (creator) lines.push(`- Creator: ${creator}`);
|
|
12634
|
-
return [...lines, ...formatCreatedAgents(profile.createdAgents, runtimeLabel)].join("\n");
|
|
12635
|
-
}
|
|
12636
|
-
/** Agent/human profile card. */
|
|
12637
|
-
function formatAgentProfile(profile, options = {}) {
|
|
12638
|
-
const runtimeLabel = options.runtimeLabel ?? agentRuntimeLabelWithStatus;
|
|
12639
|
-
return profile.kind === "human" ? formatHumanProfile(profile, runtimeLabel) : formatAgentProfileCard(profile, runtimeLabel);
|
|
12640
|
-
}
|
|
12641
|
-
//#endregion
|
|
12642
|
-
//#region ../shared/src/agentOps/channels.ts
|
|
12643
|
-
const INVALID_CHANNEL_TARGET = "Target must be a regular channel in the form '#channel-name'. DMs and thread targets are not supported.";
|
|
12644
|
-
async function resolveRegularChannel(client, target) {
|
|
12645
|
-
const name = parseRaftRegularChannelTarget$1(target ?? "");
|
|
12646
|
-
if (!name) return failureOutcome(opError("INVALID_REQUEST", { message: INVALID_CHANNEL_TARGET }));
|
|
12647
|
-
const info = await client.server.info();
|
|
12648
|
-
if (!info.ok) return failureFromClientResult(info);
|
|
12649
|
-
const channel = info.data.channels.find((candidate) => candidate.name === name);
|
|
12650
|
-
if (!channel) return failureOutcome(opError("NOT_FOUND", {
|
|
12651
|
-
message: `Channel not found: ${target}`,
|
|
12652
|
-
nextAction: "List visible channels with `raft server info --channels`; private channels need a human to add you."
|
|
12653
|
-
}));
|
|
12654
|
-
return {
|
|
12655
|
-
id: channel.id,
|
|
12656
|
-
name: channel.name,
|
|
12657
|
-
joined: channel.joined
|
|
12658
|
-
};
|
|
12659
|
-
}
|
|
12660
|
-
async function joinChannel(client, request) {
|
|
12661
|
-
const result = await joinRaftChannelByTarget$1(client, { target: request.target });
|
|
12662
|
-
if (!result.ok) {
|
|
12663
|
-
const error = result.error;
|
|
12664
|
-
if (error.kind === "validation") return error.reason === "target_not_found" ? failureOutcome(opError("NOT_FOUND", {
|
|
12665
|
-
message: error.message,
|
|
12666
|
-
nextAction: "Only visible public channels can be joined; private and joint channels need an invitation."
|
|
12667
|
-
})) : failureOutcome(opError("INVALID_REQUEST", { message: error.message }));
|
|
12668
|
-
if (error.kind === "http") return failureOutcome(opError("HTTP_ERROR", {
|
|
12669
|
-
message: error.message,
|
|
12670
|
-
status: error.status,
|
|
12671
|
-
...error.errorCode ? { serverCode: error.errorCode } : {},
|
|
12672
|
-
...error.suggestedNextAction ? { nextAction: error.suggestedNextAction } : {}
|
|
12673
|
-
}));
|
|
12674
|
-
return failureOutcome(opError("TRANSPORT_ERROR", { message: error.message }));
|
|
12675
|
-
}
|
|
12676
|
-
const state = result.data.state;
|
|
11771
|
+
//#region src/raft.ts
|
|
11772
|
+
function createRaft(options) {
|
|
11773
|
+
const serverUrl = requireServerUrl(options.serverUrl);
|
|
11774
|
+
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
11775
|
+
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
11776
|
+
const routes = createRaftRoutes({
|
|
11777
|
+
serverUrl,
|
|
11778
|
+
fetch: options.fetch,
|
|
11779
|
+
headers: options.headers,
|
|
11780
|
+
authorization,
|
|
11781
|
+
readAttempts: options.retry?.attempts,
|
|
11782
|
+
beforeRequest: options.throttle?.beforeRequest
|
|
11783
|
+
});
|
|
11784
|
+
const inboxApi = createAgentApiClient({ fetch: {
|
|
11785
|
+
baseUrl: serverUrl,
|
|
11786
|
+
fetch: (input, init) => (options.fetch ?? fetch)(input, {
|
|
11787
|
+
...init,
|
|
11788
|
+
cache: "no-store",
|
|
11789
|
+
redirect: "error"
|
|
11790
|
+
}),
|
|
11791
|
+
headers: Object.fromEntries(new Headers(options.headers)),
|
|
11792
|
+
retry: { attempts: 1 },
|
|
11793
|
+
throttle: options.throttle,
|
|
11794
|
+
auth: { authorization }
|
|
11795
|
+
} });
|
|
12677
11796
|
return {
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
command: `raft message read --target "${result.data.target}"`,
|
|
12687
|
-
args: { target: result.data.target },
|
|
12688
|
-
why: "Read the channel before posting."
|
|
11797
|
+
identity: { whoami: () => getRaftContext(routes) },
|
|
11798
|
+
wake: {
|
|
11799
|
+
verifyNotice: (input) => verifyInboxNotice(input),
|
|
11800
|
+
webhook: {
|
|
11801
|
+
status: () => webhookStatus(routes),
|
|
11802
|
+
register: (request) => registerWebhook(routes, request),
|
|
11803
|
+
unregister: () => unregisterWebhook(routes)
|
|
11804
|
+
}
|
|
12689
11805
|
},
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
const channel = await resolveRegularChannel(client, request.target);
|
|
12695
|
-
if ("ok" in channel) return channel;
|
|
12696
|
-
if (!channel.joined) return {
|
|
12697
|
-
ok: true,
|
|
12698
|
-
state: "not_joined",
|
|
12699
|
-
data: {
|
|
12700
|
-
target: request.target,
|
|
12701
|
-
channelId: channel.id
|
|
11806
|
+
inbox: {
|
|
11807
|
+
check: (request) => checkInbox(inboxApi, request, frontier),
|
|
11808
|
+
drain: (request) => drainInbox(inboxApi, request, frontier),
|
|
11809
|
+
list: (request) => listInbox(routes, request)
|
|
12702
11810
|
},
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12708
|
-
const lines = [`Left ${request.target}. You can still inspect visible public channel history there, but you can no longer send or receive ordinary channel delivery until you join the public channel again or a human re-adds you to a private channel.`];
|
|
12709
|
-
const attention = result.data.attention;
|
|
12710
|
-
if (attention?.ordinaryActivity) lines.push(attention.ordinaryActivity);
|
|
12711
|
-
return {
|
|
12712
|
-
ok: true,
|
|
12713
|
-
state: "left",
|
|
12714
|
-
data: {
|
|
12715
|
-
target: request.target,
|
|
12716
|
-
channelId: channel.id
|
|
12717
|
-
},
|
|
12718
|
-
next: null,
|
|
12719
|
-
text: lines.join("\n")
|
|
12720
|
-
};
|
|
12721
|
-
}
|
|
12722
|
-
function formatSeq(value) {
|
|
12723
|
-
return value == null ? "none" : String(value);
|
|
12724
|
-
}
|
|
12725
|
-
async function setChannelMute(client, request, action) {
|
|
12726
|
-
const channel = await resolveRegularChannel(client, request.target);
|
|
12727
|
-
if ("ok" in channel) return channel;
|
|
12728
|
-
const result = action === "mute" ? await client.channels.mute({ channelId: channel.id }, {}) : await client.channels.unmute({ channelId: channel.id });
|
|
12729
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12730
|
-
const data = result.data;
|
|
12731
|
-
const lines = [
|
|
12732
|
-
`${data.activityMuted ? "Muted" : "Unmuted"} ${request.target}.`,
|
|
12733
|
-
`Activity muted: ${data.activityMuted ? "yes" : "no"}`,
|
|
12734
|
-
`Mute from seq: ${formatSeq(data.muteFromSeq)}`
|
|
12735
|
-
];
|
|
12736
|
-
if (data.attention?.ordinaryActivity) lines.push(data.attention.ordinaryActivity);
|
|
12737
|
-
if (data.attention?.stillArrives?.length) {
|
|
12738
|
-
lines.push("Still arrives:");
|
|
12739
|
-
for (const item of data.attention.stillArrives) lines.push(`- ${item}`);
|
|
12740
|
-
}
|
|
12741
|
-
if (data.attention?.threadBoundary) lines.push(data.attention.threadBoundary);
|
|
12742
|
-
if (data.attention?.catchUp) lines.push(data.attention.catchUp);
|
|
12743
|
-
if (data.attention?.unmuteCommand) lines.push(`To unmute: ${data.attention.unmuteCommand}`);
|
|
12744
|
-
if (data.attention?.unmuteApi) lines.push(`Agent API: ${data.attention.unmuteApi}`);
|
|
12745
|
-
if (data.attention?.muteCommand) lines.push(`To mute: ${data.attention.muteCommand}`);
|
|
12746
|
-
if (data.attention?.muteApi) lines.push(`Agent API: ${data.attention.muteApi}`);
|
|
12747
|
-
return {
|
|
12748
|
-
ok: true,
|
|
12749
|
-
state: data.activityMuted ? "muted" : "unmuted",
|
|
12750
|
-
data: {
|
|
12751
|
-
target: request.target,
|
|
12752
|
-
channelId: channel.id,
|
|
12753
|
-
activityMuted: data.activityMuted === true,
|
|
12754
|
-
muteFromSeq: data.muteFromSeq ?? null,
|
|
12755
|
-
stillArrives: data.attention?.stillArrives ?? []
|
|
12756
|
-
},
|
|
12757
|
-
next: null,
|
|
12758
|
-
text: lines.join("\n")
|
|
12759
|
-
};
|
|
12760
|
-
}
|
|
12761
|
-
function muteChannel(client, request) {
|
|
12762
|
-
return setChannelMute(client, request, "mute");
|
|
12763
|
-
}
|
|
12764
|
-
function unmuteChannel(client, request) {
|
|
12765
|
-
return setChannelMute(client, request, "unmute");
|
|
12766
|
-
}
|
|
12767
|
-
async function channelMembers(client, request) {
|
|
12768
|
-
if (!request.target?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel, DM, or thread target is required." }));
|
|
12769
|
-
const result = await client.channels.members({ channel: request.target });
|
|
12770
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12771
|
-
return {
|
|
12772
|
-
ok: true,
|
|
12773
|
-
state: "members",
|
|
12774
|
-
data: result.data,
|
|
12775
|
-
next: null,
|
|
12776
|
-
text: formatAgentChannelMembers(result.data)
|
|
12777
|
-
};
|
|
12778
|
-
}
|
|
12779
|
-
async function listThreads(client) {
|
|
12780
|
-
const result = await client.threads.list();
|
|
12781
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12782
|
-
const threads = result.data.threads;
|
|
12783
|
-
return {
|
|
12784
|
-
ok: true,
|
|
12785
|
-
state: threads.length > 0 ? "threads" : "empty",
|
|
12786
|
-
data: threads,
|
|
12787
|
-
next: null,
|
|
12788
|
-
text: formatAgentThreadList(threads)
|
|
12789
|
-
};
|
|
12790
|
-
}
|
|
12791
|
-
async function unfollowThread(client, request) {
|
|
12792
|
-
if (!request.target?.includes(":")) return failureOutcome(opError("INVALID_REQUEST", { message: "A thread target is required, for example '#channel:shortid' or 'dm:@peer:shortid'." }));
|
|
12793
|
-
const result = await client.threads.unfollow({
|
|
12794
|
-
thread: request.target,
|
|
12795
|
-
...request.reason ? { reason: request.reason } : {}
|
|
12796
|
-
});
|
|
12797
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12798
|
-
return {
|
|
12799
|
-
ok: true,
|
|
12800
|
-
state: "unfollowed",
|
|
12801
|
-
data: { target: request.target },
|
|
12802
|
-
next: null,
|
|
12803
|
-
text: `Unfollowed ${request.target}. Ordinary delivery from this thread stops; a direct @mention reactivates the follow.`
|
|
12804
|
-
};
|
|
12805
|
-
}
|
|
12806
|
-
//#endregion
|
|
12807
|
-
//#region ../shared/src/agentOps/server.ts
|
|
12808
|
-
function nextCommand(section, request, offset, limit, total) {
|
|
12809
|
-
const nextOffset = offset + limit;
|
|
12810
|
-
if (nextOffset >= total) return void 0;
|
|
12811
|
-
const parts = [
|
|
12812
|
-
"raft server info",
|
|
12813
|
-
`--${section}`,
|
|
12814
|
-
`--offset ${nextOffset}`,
|
|
12815
|
-
`--limit ${limit}`
|
|
12816
|
-
];
|
|
12817
|
-
if (section === "channels" && request.joined) parts.push("--joined");
|
|
12818
|
-
return parts.join(" ");
|
|
12819
|
-
}
|
|
12820
|
-
async function serverInfo(client, request = {}) {
|
|
12821
|
-
const result = await client.server.info();
|
|
12822
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12823
|
-
const server = result.data;
|
|
12824
|
-
const view = request.view ?? "summary";
|
|
12825
|
-
if (view === "summary") return {
|
|
12826
|
-
ok: true,
|
|
12827
|
-
state: "info",
|
|
12828
|
-
data: {
|
|
12829
|
-
view,
|
|
12830
|
-
server,
|
|
12831
|
-
page: null
|
|
12832
|
-
},
|
|
12833
|
-
next: {
|
|
12834
|
-
kind: "list_channels",
|
|
12835
|
-
command: "raft server info --channels",
|
|
12836
|
-
why: "The summary only counts; list a section to see names."
|
|
12837
|
-
},
|
|
12838
|
-
text: formatAgentServerSummary(server)
|
|
12839
|
-
};
|
|
12840
|
-
if (view === "full") return {
|
|
12841
|
-
ok: true,
|
|
12842
|
-
state: "info",
|
|
12843
|
-
data: {
|
|
12844
|
-
view,
|
|
12845
|
-
server,
|
|
12846
|
-
page: null
|
|
12847
|
-
},
|
|
12848
|
-
next: null,
|
|
12849
|
-
text: formatAgentServerInfo(server)
|
|
12850
|
-
};
|
|
12851
|
-
const limit = Math.max(1, Math.trunc(request.limit ?? 50));
|
|
12852
|
-
const offset = Math.max(0, Math.trunc(request.offset ?? 0));
|
|
12853
|
-
const rows = view === "channels" ? server.channels.filter((c) => !request.joined || c.joined) : view === "agents" ? server.agents : server.humans;
|
|
12854
|
-
const pageRows = rows.slice(offset, offset + limit);
|
|
12855
|
-
const page = {
|
|
12856
|
-
section: view,
|
|
12857
|
-
total: rows.length,
|
|
12858
|
-
offset,
|
|
12859
|
-
limit,
|
|
12860
|
-
nextCommand: nextCommand(view, request, offset, limit, rows.length)
|
|
12861
|
-
};
|
|
12862
|
-
const text = view === "channels" ? formatAgentServerChannels(pageRows, page) : view === "agents" ? formatAgentServerAgents(pageRows, page) : formatAgentServerHumans(pageRows, page);
|
|
12863
|
-
const next = page.nextCommand ? {
|
|
12864
|
-
kind: "next_page",
|
|
12865
|
-
command: page.nextCommand,
|
|
12866
|
-
args: {
|
|
12867
|
-
view,
|
|
12868
|
-
offset: offset + limit,
|
|
12869
|
-
limit
|
|
12870
|
-
},
|
|
12871
|
-
why: "More rows exist; one page is one page."
|
|
12872
|
-
} : null;
|
|
12873
|
-
return {
|
|
12874
|
-
ok: true,
|
|
12875
|
-
state: "info",
|
|
12876
|
-
data: {
|
|
12877
|
-
view,
|
|
12878
|
-
server,
|
|
12879
|
-
page
|
|
12880
|
-
},
|
|
12881
|
-
next,
|
|
12882
|
-
text
|
|
12883
|
-
};
|
|
12884
|
-
}
|
|
12885
|
-
async function showProfile(client, request = {}, options = {}) {
|
|
12886
|
-
const result = await client.profile.show(request.target ? { target: request.target } : {});
|
|
12887
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12888
|
-
return {
|
|
12889
|
-
ok: true,
|
|
12890
|
-
state: "profile",
|
|
12891
|
-
data: result.data,
|
|
12892
|
-
next: null,
|
|
12893
|
-
text: formatAgentProfile(result.data, options)
|
|
12894
|
-
};
|
|
12895
|
-
}
|
|
12896
|
-
async function updateProfile(client, request, options = {}) {
|
|
12897
|
-
if (request.displayName === void 0 && request.description === void 0 && request.avatarUrl === void 0) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass a displayName, description, or avatarUrl to update." }));
|
|
12898
|
-
const result = await client.profile.update(request);
|
|
12899
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12900
|
-
return {
|
|
12901
|
-
ok: true,
|
|
12902
|
-
state: "updated",
|
|
12903
|
-
data: result.data,
|
|
12904
|
-
next: null,
|
|
12905
|
-
text: formatAgentProfile(result.data, options)
|
|
12906
|
-
};
|
|
12907
|
-
}
|
|
12908
|
-
//#endregion
|
|
12909
|
-
//#region ../shared/src/agentText/attachments.ts
|
|
12910
|
-
/** Upload receipt with attachment id and send-usage hint. */
|
|
12911
|
-
function formatAgentAttachmentUploaded(attachment) {
|
|
12912
|
-
return `File uploaded: ${attachment.filename} (${(attachment.sizeBytes / 1024).toFixed(1)}KB)\nAttachment ID: ${attachment.id}\n\nUse this ID with raft message send --attachment-id ${attachment.id} to include it in a message.\n`;
|
|
12913
|
-
}
|
|
12914
|
-
function anchorSummary(anchor) {
|
|
12915
|
-
if (anchor.type === "md-section") {
|
|
12916
|
-
const title = anchor.data.headingTitle ?? anchor.data.headingId;
|
|
12917
|
-
return typeof title === "string" && title ? `§ ${title}` : "§ section";
|
|
12918
|
-
}
|
|
12919
|
-
if (anchor.type === "lines" || anchor.type === "csv-rows") {
|
|
12920
|
-
const start = Number(anchor.data.start);
|
|
12921
|
-
const end = Number(anchor.data.end ?? start);
|
|
12922
|
-
if (!Number.isFinite(start)) return anchor.type;
|
|
12923
|
-
const prefix = anchor.type === "lines" ? "L" : "rows ";
|
|
12924
|
-
return start === (Number.isFinite(end) ? end : start) ? `${prefix}${start}` : `${prefix}${start}–${end}`;
|
|
12925
|
-
}
|
|
12926
|
-
if (anchor.type === "html-region") {
|
|
12927
|
-
const quote = anchor.data.quote;
|
|
12928
|
-
if (typeof quote === "string" && quote.trim().length > 0) {
|
|
12929
|
-
const trimmed = quote.trim();
|
|
12930
|
-
return trimmed.length > 60 ? `${trimmed.slice(0, 60)}…` : trimmed;
|
|
12931
|
-
}
|
|
12932
|
-
return "HTML region";
|
|
12933
|
-
}
|
|
12934
|
-
return anchor.type;
|
|
12935
|
-
}
|
|
12936
|
-
/** Attachment-scoped comment list incl. anchors/reactions, or its empty state. */
|
|
12937
|
-
function formatAgentAttachmentComments(attachmentId, comments, threadChannelId) {
|
|
12938
|
-
if (comments.length === 0) return `No comments on attachment ${attachmentId.slice(0, 8)}.\n`;
|
|
12939
|
-
const lines = [`## Comments on attachment ${attachmentId.slice(0, 8)} (${comments.length})`];
|
|
12940
|
-
for (const c of comments) {
|
|
12941
|
-
const check = c.reactions.some((r) => r.emoji === "✅") ? " ✅" : "";
|
|
12942
|
-
const anchor = c.anchor ? ` [anchor: ${anchorSummary(c.anchor)}]` : "";
|
|
12943
|
-
lines.push(`[msg=${c.id.slice(0, 8)} time=${c.createdAt} type=${c.senderType}]${check}${anchor} @${c.senderName}: ${c.content}`);
|
|
12944
|
-
}
|
|
12945
|
-
if (threadChannelId) lines.push(`(full conversation lives in thread channel ${threadChannelId})`);
|
|
12946
|
-
return lines.join("\n") + "\n";
|
|
12947
|
-
}
|
|
12948
|
-
//#endregion
|
|
12949
|
-
//#region ../shared/src/agentOps/attachments.ts
|
|
12950
|
-
/** Compatibility fallback for Servers that predate the capability endpoint (the CLI's constant). */
|
|
12951
|
-
const AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES = 52428800;
|
|
12952
|
-
const FILENAME_MIME_MAP = {
|
|
12953
|
-
".jpg": "image/jpeg",
|
|
12954
|
-
".jpeg": "image/jpeg",
|
|
12955
|
-
".png": "image/png",
|
|
12956
|
-
".gif": "image/gif",
|
|
12957
|
-
".webp": "image/webp",
|
|
12958
|
-
".pdf": "application/pdf",
|
|
12959
|
-
".txt": "text/plain",
|
|
12960
|
-
".md": "text/markdown",
|
|
12961
|
-
".json": "application/json",
|
|
12962
|
-
".csv": "text/csv"
|
|
12963
|
-
};
|
|
12964
|
-
const MIME_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
12965
|
-
function sniffMimeType(bytes) {
|
|
12966
|
-
const starts = (sig) => sig.every((b, i) => bytes[i] === b);
|
|
12967
|
-
if (bytes.length >= 8 && starts([
|
|
12968
|
-
137,
|
|
12969
|
-
80,
|
|
12970
|
-
78,
|
|
12971
|
-
71,
|
|
12972
|
-
13,
|
|
12973
|
-
10,
|
|
12974
|
-
26,
|
|
12975
|
-
10
|
|
12976
|
-
])) return "image/png";
|
|
12977
|
-
if (bytes.length >= 3 && starts([
|
|
12978
|
-
255,
|
|
12979
|
-
216,
|
|
12980
|
-
255
|
|
12981
|
-
])) return "image/jpeg";
|
|
12982
|
-
if (bytes.length >= 6) {
|
|
12983
|
-
const header = String.fromCharCode(...bytes.subarray(0, 6));
|
|
12984
|
-
if (header === "GIF87a" || header === "GIF89a") return "image/gif";
|
|
12985
|
-
}
|
|
12986
|
-
if (bytes.length >= 12 && String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP") return "image/webp";
|
|
12987
|
-
return null;
|
|
12988
|
-
}
|
|
12989
|
-
/** Explicit type wins; otherwise sniff the bytes, then the filename extension, then octet-stream. */
|
|
12990
|
-
function inferAttachmentMimeType(filename, bytes, explicit) {
|
|
12991
|
-
const normalized = explicit?.trim().toLowerCase();
|
|
12992
|
-
if (normalized && MIME_TYPE_RE.test(normalized)) return normalized;
|
|
12993
|
-
const dot = filename.lastIndexOf(".");
|
|
12994
|
-
const ext = dot >= 0 ? filename.slice(dot).toLowerCase() : "";
|
|
12995
|
-
return sniffMimeType(bytes) ?? FILENAME_MIME_MAP[ext] ?? "application/octet-stream";
|
|
12996
|
-
}
|
|
12997
|
-
/**
|
|
12998
|
-
* Upload bytes into a conversation, choosing the path the CLI would: multipart
|
|
12999
|
-
* `POST /upload` below the Server's direct-upload threshold, an upload session
|
|
13000
|
-
* (create → PUT to a presigned URL → complete) at or above it.
|
|
13001
|
-
*/
|
|
13002
|
-
async function uploadAttachment(client, transport, request) {
|
|
13003
|
-
if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
|
|
13004
|
-
if (!(request.bytes instanceof Uint8Array) || request.bytes.byteLength === 0) return failureOutcome(opError("INVALID_REQUEST", { message: "Refusing to upload a 0-byte attachment." }));
|
|
13005
|
-
if (request.mimeType !== void 0 && !MIME_TYPE_RE.test(request.mimeType.trim())) return failureOutcome(opError("INVALID_REQUEST", { message: `mimeType must look like type/subtype, got: ${request.mimeType}` }));
|
|
13006
|
-
const capability = await client.attachments.uploadCapabilities();
|
|
13007
|
-
const maxBytes = capability.ok ? capability.data.maxBytes : AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES;
|
|
13008
|
-
const threshold = capability.ok && capability.data.directUploadEnabled ? capability.data.directUploadThresholdBytes : null;
|
|
13009
|
-
if (request.bytes.byteLength > maxBytes) return failureOutcome(opError("INVALID_REQUEST", { message: `Attachment is ${request.bytes.byteLength} bytes; the Server's maximum is ${maxBytes}.` }));
|
|
13010
|
-
const resolved = await client.channels.resolve({ target: request.target });
|
|
13011
|
-
if (!resolved.ok) return failureFromClientResult(resolved);
|
|
13012
|
-
const channelId = resolved.data.channelId;
|
|
13013
|
-
const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
|
|
13014
|
-
if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
|
|
13015
|
-
const copy = new Uint8Array(request.bytes.byteLength);
|
|
13016
|
-
copy.set(request.bytes);
|
|
13017
|
-
const form = new FormData();
|
|
13018
|
-
form.append("file", new Blob([copy.buffer], { type: mimeType }), request.filename);
|
|
13019
|
-
form.append("channelId", channelId);
|
|
13020
|
-
if (request.mimeType) form.append("mimeType", mimeType);
|
|
13021
|
-
const headers = new Headers(transport.headers);
|
|
13022
|
-
headers.delete("content-type");
|
|
13023
|
-
headers.set("accept", "application/json");
|
|
13024
|
-
headers.set("authorization", transport.authorization);
|
|
13025
|
-
let response;
|
|
13026
|
-
try {
|
|
13027
|
-
response = await transport.fetch(`${transport.serverUrl}${agentApiContract.attachmentUpload.fullPath}`, {
|
|
13028
|
-
method: "POST",
|
|
13029
|
-
headers,
|
|
13030
|
-
body: form
|
|
13031
|
-
});
|
|
13032
|
-
} catch {
|
|
13033
|
-
return failureOutcome(opError("TRANSPORT_ERROR"));
|
|
13034
|
-
}
|
|
13035
|
-
let body = null;
|
|
13036
|
-
try {
|
|
13037
|
-
body = await response.json();
|
|
13038
|
-
} catch {
|
|
13039
|
-
body = null;
|
|
13040
|
-
}
|
|
13041
|
-
if (!response.ok) {
|
|
13042
|
-
const b = body && typeof body === "object" ? body : {};
|
|
13043
|
-
return failureOutcome(opErrorFromClientError({
|
|
13044
|
-
kind: "http",
|
|
13045
|
-
reason: "http_error",
|
|
13046
|
-
message: typeof b.error === "string" ? b.error : `HTTP ${response.status}`,
|
|
13047
|
-
status: response.status,
|
|
13048
|
-
errorCode: typeof b.errorCode === "string" ? b.errorCode : typeof b.code === "string" ? b.code : null,
|
|
13049
|
-
suggestedNextAction: typeof b.suggestedNextAction === "string" ? b.suggestedNextAction : null
|
|
13050
|
-
}));
|
|
13051
|
-
}
|
|
13052
|
-
const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
|
|
13053
|
-
if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
|
|
13054
|
-
return uploadedOutcome(request.target, parsed.data, mimeType);
|
|
13055
|
-
}
|
|
13056
|
-
function uploadedOutcome(target, data, mimeType) {
|
|
13057
|
-
return {
|
|
13058
|
-
ok: true,
|
|
13059
|
-
state: "uploaded",
|
|
13060
|
-
data: {
|
|
13061
|
-
...data,
|
|
13062
|
-
target,
|
|
13063
|
-
mimeType: data.mimeType ?? mimeType
|
|
13064
|
-
},
|
|
13065
|
-
next: {
|
|
13066
|
-
kind: "send_with_attachment",
|
|
13067
|
-
command: `raft message send --target "${target}" --attachment-id ${data.id}`,
|
|
13068
|
-
args: {
|
|
13069
|
-
target,
|
|
13070
|
-
attachmentIds: [data.id]
|
|
13071
|
-
},
|
|
13072
|
-
why: "The upload alone posts nothing; send a message that links the attachment id."
|
|
13073
|
-
},
|
|
13074
|
-
text: formatAgentAttachmentUploaded(data)
|
|
13075
|
-
};
|
|
13076
|
-
}
|
|
13077
|
-
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13078
|
-
/**
|
|
13079
|
-
* Direct upload for files at or above the Server's threshold, mirroring the
|
|
13080
|
-
* CLI: create a session, PUT the bytes to the presigned URL (fetch; one retry
|
|
13081
|
-
* when the object store may have committed), then complete, which the Server
|
|
13082
|
-
* verifies against the object store. A PUT that definitely failed cancels the
|
|
13083
|
-
* session; a PUT whose outcome is unknown leaves it for completion to verify.
|
|
13084
|
-
*/
|
|
13085
|
-
async function uploadThroughSession(client, transport, request, channelId, mimeType) {
|
|
13086
|
-
const created = await client.attachments.createUploadSession({
|
|
13087
|
-
channelId,
|
|
13088
|
-
filename: request.filename,
|
|
13089
|
-
mimeType,
|
|
13090
|
-
sizeBytes: request.bytes.byteLength,
|
|
13091
|
-
clientRequestId: crypto.randomUUID()
|
|
13092
|
-
});
|
|
13093
|
-
if (!created.ok) return failureFromClientResult(created);
|
|
13094
|
-
const { uploadId, upload } = created.data;
|
|
13095
|
-
const body = new Uint8Array(request.bytes.byteLength);
|
|
13096
|
-
body.set(request.bytes);
|
|
13097
|
-
let definitelyFailed = null;
|
|
13098
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
13099
|
-
let response;
|
|
13100
|
-
try {
|
|
13101
|
-
response = await transport.fetch(upload.url, {
|
|
13102
|
-
method: upload.method ?? "PUT",
|
|
13103
|
-
headers: { ...upload.headers },
|
|
13104
|
-
body: body.buffer,
|
|
13105
|
-
redirect: "error"
|
|
13106
|
-
});
|
|
13107
|
-
} catch {
|
|
13108
|
-
if (attempt === 0) continue;
|
|
13109
|
-
break;
|
|
13110
|
-
}
|
|
13111
|
-
if (response.ok || response.status === 412) {
|
|
13112
|
-
definitelyFailed = null;
|
|
13113
|
-
break;
|
|
13114
|
-
}
|
|
13115
|
-
const mayExist = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
13116
|
-
if (mayExist && attempt === 0) continue;
|
|
13117
|
-
if (!mayExist) definitelyFailed = response.status;
|
|
13118
|
-
break;
|
|
13119
|
-
}
|
|
13120
|
-
if (definitelyFailed !== null) {
|
|
13121
|
-
await client.attachments.cancelUploadSession({ uploadId }).catch(() => void 0);
|
|
13122
|
-
return failureOutcome(opError("HTTP_ERROR", {
|
|
13123
|
-
message: `Direct object upload failed with HTTP ${definitelyFailed}; the upload session was cancelled.`,
|
|
13124
|
-
status: definitelyFailed,
|
|
13125
|
-
nextAction: "Retry the upload; nothing was attached."
|
|
13126
|
-
}));
|
|
13127
|
-
}
|
|
13128
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
13129
|
-
const completed = await client.attachments.completeUploadSession({ uploadId });
|
|
13130
|
-
if (completed.ok) {
|
|
13131
|
-
const attachment = completed.data.attachment;
|
|
13132
|
-
return uploadedOutcome(request.target, attachment, mimeType);
|
|
13133
|
-
}
|
|
13134
|
-
const code = completed.error.kind === "http" ? completed.error.errorCode : null;
|
|
13135
|
-
if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
|
|
13136
|
-
await wait(250 * (attempt + 1));
|
|
13137
|
-
}
|
|
13138
|
-
return failureOutcome(opError("UNAVAILABLE", { message: "Direct upload completion ended without a terminal response." }));
|
|
13139
|
-
}
|
|
13140
|
-
async function downloadAttachment(client, request) {
|
|
13141
|
-
if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
|
|
13142
|
-
const result = await client.attachments.download({ attachmentId: request.attachmentId });
|
|
13143
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13144
|
-
const bytes = result.data;
|
|
13145
|
-
return {
|
|
13146
|
-
ok: true,
|
|
13147
|
-
state: "downloaded",
|
|
13148
|
-
data: {
|
|
13149
|
-
attachmentId: request.attachmentId,
|
|
13150
|
-
bytes
|
|
13151
|
-
},
|
|
13152
|
-
next: null,
|
|
13153
|
-
text: `Downloaded attachment ${request.attachmentId.slice(0, 8)} (${bytes.byteLength} bytes).`
|
|
13154
|
-
};
|
|
13155
|
-
}
|
|
13156
|
-
async function attachmentComments(client, request) {
|
|
13157
|
-
if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
|
|
13158
|
-
const result = await client.attachments.comments({ attachmentId: request.attachmentId }, request.limit === void 0 ? {} : { limit: String(request.limit) });
|
|
13159
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13160
|
-
const data = result.data;
|
|
13161
|
-
return {
|
|
13162
|
-
ok: true,
|
|
13163
|
-
state: data.comments.length > 0 ? "comments" : "empty",
|
|
13164
|
-
data: {
|
|
13165
|
-
...data,
|
|
13166
|
-
attachmentId: request.attachmentId
|
|
13167
|
-
},
|
|
13168
|
-
next: null,
|
|
13169
|
-
text: formatAgentAttachmentComments(request.attachmentId, data.comments, data.threadChannelId)
|
|
13170
|
-
};
|
|
13171
|
-
}
|
|
13172
|
-
//#endregion
|
|
13173
|
-
//#region ../shared/src/agentText/search.ts
|
|
13174
|
-
/** Local wall-clock rendering with offset, as the CLI prints search hit times. */
|
|
13175
|
-
function formatLocalTimeWithOffset(iso) {
|
|
13176
|
-
const d = new Date(iso);
|
|
13177
|
-
if (isNaN(d.getTime())) return iso;
|
|
13178
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
13179
|
-
const offsetMinutes = -d.getTimezoneOffset();
|
|
13180
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
13181
|
-
const absOffset = Math.abs(offsetMinutes);
|
|
13182
|
-
const offset = `${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`;
|
|
13183
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} ${offset}`;
|
|
13184
|
-
}
|
|
13185
|
-
function renderSearchSource(result) {
|
|
13186
|
-
if (result.channelType === "thread") {
|
|
13187
|
-
const shortId = typeof result.channelName === "string" && result.channelName.startsWith("thread-") ? result.channelName.slice(7) : typeof result.threadId === "string" && result.threadId ? result.threadId.slice(0, 8) : result.channelName;
|
|
13188
|
-
if (result.parentChannelType === "dm") return `dm:${neutralizeAgentRaftRefLiterals(result.parentChannelName ?? "unknown")}:${shortId}`;
|
|
13189
|
-
return `thread:${neutralizeAgentRaftRefLiterals(result.parentChannelName ?? "unknown")}:${shortId}`;
|
|
13190
|
-
}
|
|
13191
|
-
if (result.channelType === "dm") return `dm:${neutralizeAgentRaftRefLiterals(result.channelName ?? "unknown")}`;
|
|
13192
|
-
return `channel:${neutralizeAgentRaftRefLiterals(result.channelName ?? "unknown")}`;
|
|
13193
|
-
}
|
|
13194
|
-
const PREVIEW_BEFORE_CHARS = 80;
|
|
13195
|
-
const PREVIEW_AFTER_CHARS = 120;
|
|
13196
|
-
const PREVIEW_FALLBACK_CHARS = 200;
|
|
13197
|
-
function escapeRegExp(value) {
|
|
13198
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13199
|
-
}
|
|
13200
|
-
function findSearchMatch(content, query) {
|
|
13201
|
-
const normalizedQuery = query.trim();
|
|
13202
|
-
if (!normalizedQuery) return null;
|
|
13203
|
-
const exactIndex = content.toLowerCase().indexOf(normalizedQuery.toLowerCase());
|
|
13204
|
-
if (exactIndex >= 0) return {
|
|
13205
|
-
start: exactIndex,
|
|
13206
|
-
end: exactIndex + normalizedQuery.length
|
|
13207
|
-
};
|
|
13208
|
-
const terms = normalizedQuery.match(/"([^"]+)"|\S+/g) ?? [];
|
|
13209
|
-
for (const rawTerm of terms) {
|
|
13210
|
-
const term = rawTerm.replace(/^"|"$/g, "").trim();
|
|
13211
|
-
if (!term) continue;
|
|
13212
|
-
const match = new RegExp(escapeRegExp(term), "i").exec(content);
|
|
13213
|
-
if (match?.index !== void 0) return {
|
|
13214
|
-
start: match.index,
|
|
13215
|
-
end: match.index + match[0].length
|
|
13216
|
-
};
|
|
13217
|
-
}
|
|
13218
|
-
return null;
|
|
13219
|
-
}
|
|
13220
|
-
function* findRaftRefLiteralRanges(content) {
|
|
13221
|
-
for (const match of content.matchAll(/\bdm:@[A-Za-z0-9][A-Za-z0-9_-]*(?:~(?:agent|human))?/g)) if (match.index !== void 0) yield {
|
|
13222
|
-
start: match.index,
|
|
13223
|
-
end: match.index + match[0].length
|
|
13224
|
-
};
|
|
13225
|
-
for (const match of content.matchAll(/\btask #[0-9]+\b/g)) if (match.index !== void 0) yield {
|
|
13226
|
-
start: match.index,
|
|
13227
|
-
end: match.index + match[0].length
|
|
13228
|
-
};
|
|
13229
|
-
for (const match of content.matchAll(/(^|[\n\s([{"'`;])(@[A-Za-z0-9][A-Za-z0-9_-]*)/g)) {
|
|
13230
|
-
if (match.index === void 0) continue;
|
|
13231
|
-
const prefix = match[1] ?? "";
|
|
13232
|
-
const ref = match[2] ?? "";
|
|
13233
|
-
yield {
|
|
13234
|
-
start: match.index + prefix.length,
|
|
13235
|
-
end: match.index + prefix.length + ref.length
|
|
13236
|
-
};
|
|
13237
|
-
}
|
|
13238
|
-
for (const match of content.matchAll(/(^|[\n\s([{"'`;])(#[A-Za-z][A-Za-z0-9_-]*)/g)) {
|
|
13239
|
-
if (match.index === void 0) continue;
|
|
13240
|
-
const prefix = match[1] ?? "";
|
|
13241
|
-
const ref = match[2] ?? "";
|
|
13242
|
-
yield {
|
|
13243
|
-
start: match.index + prefix.length,
|
|
13244
|
-
end: match.index + prefix.length + ref.length
|
|
13245
|
-
};
|
|
13246
|
-
}
|
|
13247
|
-
}
|
|
13248
|
-
function expandSearchMatchToRefLiteral(content, match) {
|
|
13249
|
-
for (const refRange of findRaftRefLiteralRanges(content)) if (match.start < refRange.end && match.end > refRange.start) return {
|
|
13250
|
-
start: Math.min(match.start, refRange.start),
|
|
13251
|
-
end: Math.max(match.end, refRange.end)
|
|
13252
|
-
};
|
|
13253
|
-
return match;
|
|
13254
|
-
}
|
|
13255
|
-
function trimPreviewWindow(content, start, end) {
|
|
13256
|
-
let trimmedStart = start;
|
|
13257
|
-
let trimmedEnd = end;
|
|
13258
|
-
while (trimmedStart > 0 && /\s/.test(content[trimmedStart] ?? "")) trimmedStart += 1;
|
|
13259
|
-
while (trimmedEnd < content.length && /\s/.test(content[trimmedEnd - 1] ?? "")) trimmedEnd -= 1;
|
|
13260
|
-
return {
|
|
13261
|
-
start: Math.max(0, Math.min(trimmedStart, content.length)),
|
|
13262
|
-
end: Math.max(0, Math.min(trimmedEnd, content.length))
|
|
13263
|
-
};
|
|
13264
|
-
}
|
|
13265
|
-
function escapeSearchComponentLiterals(text) {
|
|
13266
|
-
return text.replace(/<\/?(?:result|preview|match)\b[^>]*>|<omit\s*\/>/gi, (tag) => tag.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"));
|
|
13267
|
-
}
|
|
13268
|
-
/** Neutralise `@handle`, `#channel`, `dm:@peer`, and `task #N` literals so quoted text cannot route attention. */
|
|
13269
|
-
function neutralizeAgentRaftRefLiterals(text) {
|
|
13270
|
-
return text.replace(/\bdm:@([A-Za-z0-9][A-Za-z0-9_-]*(?:~(?:agent|human))?)/g, "dm:user:$1").replace(/\btask #([0-9]+)\b/g, "task:$1").replace(/(^|[\n\s([{"'`;])@([A-Za-z0-9][A-Za-z0-9_-]*)/g, "$1user:$2").replace(/(^|[\n\s([{"'`;])#([A-Za-z][A-Za-z0-9_-]*)/g, "$1channel:$2");
|
|
13271
|
-
}
|
|
13272
|
-
function renderAgentSearchPreviewText(text) {
|
|
13273
|
-
return neutralizeAgentRaftRefLiterals(escapeSearchComponentLiterals(text));
|
|
13274
|
-
}
|
|
13275
|
-
function renderSearchPreview(content, query) {
|
|
13276
|
-
const foundMatch = findSearchMatch(content, query);
|
|
13277
|
-
const match = foundMatch ? expandSearchMatchToRefLiteral(content, foundMatch) : null;
|
|
13278
|
-
let start = 0;
|
|
13279
|
-
let end = Math.min(content.length, PREVIEW_FALLBACK_CHARS);
|
|
13280
|
-
if (match) {
|
|
13281
|
-
start = Math.max(0, match.start - PREVIEW_BEFORE_CHARS);
|
|
13282
|
-
end = Math.min(content.length, match.end + PREVIEW_AFTER_CHARS);
|
|
13283
|
-
}
|
|
13284
|
-
({start, end} = trimPreviewWindow(content, start, end));
|
|
13285
|
-
const leadingOmit = start > 0 ? "<omit />" : "";
|
|
13286
|
-
const trailingOmit = end < content.length ? "<omit />" : "";
|
|
13287
|
-
if (!match || match.end <= start || match.start >= end) return `${leadingOmit}${renderAgentSearchPreviewText(content.slice(start, end))}${trailingOmit}`;
|
|
13288
|
-
const before = content.slice(start, match.start);
|
|
13289
|
-
const matched = content.slice(match.start, match.end);
|
|
13290
|
-
const after = content.slice(match.end, end);
|
|
13291
|
-
return [
|
|
13292
|
-
leadingOmit,
|
|
13293
|
-
renderAgentSearchPreviewText(before),
|
|
13294
|
-
"<match>",
|
|
13295
|
-
renderAgentSearchPreviewText(matched),
|
|
13296
|
-
"</match>",
|
|
13297
|
-
renderAgentSearchPreviewText(after),
|
|
13298
|
-
trailingOmit
|
|
13299
|
-
].join("");
|
|
13300
|
-
}
|
|
13301
|
-
/** Search results with <match>/<omit /> preview markup. */
|
|
13302
|
-
function formatAgentSearchResults(query, data, offset, sort, limit) {
|
|
13303
|
-
const trimmedQuery = query.trim();
|
|
13304
|
-
const oldestShown = data.results?.length ? data.results[data.results.length - 1]?.createdAt : void 0;
|
|
13305
|
-
const nextPageHint = sort === "recent" && oldestShown ? `page with --before ${oldestShown} (pages OLDER only; NOT a complete traversal: stored times are microsecond but this key is millisecond, so rows inside the boundary millisecond can be skipped, and a full page sharing one timestamp can repeat indefinitely; copy the key verbatim)` : `page with --offset ${(offset ?? 0) + (data.results?.length ?? 0)}`;
|
|
13306
|
-
const effectiveLimit = Math.min(limit ?? 20, 50);
|
|
13307
|
-
const countEqualsLimit = (data.results?.length ?? 0) === effectiveLimit;
|
|
13308
|
-
const limitSource = limit === void 0 ? `the server default of ${effectiveLimit}` : limit > 50 ? `the server cap of ${effectiveLimit} (your --limit ${limit} was clamped)` : `the --limit ${effectiveLimit} that was requested`;
|
|
13309
|
-
const atCapRemedy = effectiveLimit < 50 ? "re-run with a higher --limit to tell the two apart" : `this is already the server's maximum page, so completeness CANNOT be determined from this result: a higher --limit is clamped back to ${effectiveLimit} and paging will not reveal it; narrow the query (add --sender, --target, --after or --before) until fewer than ${effectiveLimit} results match`;
|
|
13310
|
-
const truncation = data.hasMore === true ? `truncated=true · more results exist, ${nextPageHint}` : data.hasMore === false ? countEqualsLimit ? `truncated=unknown · server reported hasMore=false, but this page returned exactly ${limitSource}, which is also what a capped page returns; ${atCapRemedy}` : "truncated=false" : "truncated=unknown · server did not report hasMore";
|
|
13311
|
-
if (!data.results || data.results.length === 0) return `No search results. (${truncation})`;
|
|
13312
|
-
const formatted = data.results.map((result) => {
|
|
13313
|
-
const ref = `msg:${result.id}`;
|
|
13314
|
-
const content = result.content ?? result.snippet ?? "";
|
|
13315
|
-
const sender = neutralizeAgentRaftRefLiterals(result.senderName ?? "unknown");
|
|
13316
|
-
const senderType = result.senderType ? ` (${result.senderType})` : "";
|
|
13317
|
-
const taskProjection = formatAgentTaskCurrentProjection(result.taskCurrentProjection, result.taskNumber, renderAgentSearchPreviewText).trimStart();
|
|
13318
|
-
return [
|
|
13319
|
-
`<result ref="${ref}">`,
|
|
13320
|
-
`Source: ${renderSearchSource(result)}`,
|
|
13321
|
-
`Sender: ${sender}${senderType}`,
|
|
13322
|
-
`Time: ${result.createdAt ? formatLocalTimeWithOffset(result.createdAt) : "-"}`,
|
|
13323
|
-
...taskProjection ? [taskProjection] : [],
|
|
13324
|
-
"",
|
|
13325
|
-
"<preview>",
|
|
13326
|
-
renderSearchPreview(content, trimmedQuery),
|
|
13327
|
-
"</preview>",
|
|
13328
|
-
"</result>"
|
|
13329
|
-
].join("\n");
|
|
13330
|
-
}).join("\n\n");
|
|
13331
|
-
const resultLabel = data.results.length === 1 ? "result" : "results";
|
|
13332
|
-
const count = `${data.results.length} ${resultLabel} · ${truncation}`;
|
|
13333
|
-
return [
|
|
13334
|
-
trimmedQuery ? `Search results for: "${trimmedQuery}" (${count})` : `Filtered message results (${count})`,
|
|
13335
|
-
"",
|
|
13336
|
-
formatted,
|
|
13337
|
-
"",
|
|
13338
|
-
"If a result may be relevant but its preview is not enough, read the surrounding context for that result before answering."
|
|
13339
|
-
].join("\n");
|
|
13340
|
-
}
|
|
13341
|
-
//#endregion
|
|
13342
|
-
//#region ../shared/src/agentOps/search.ts
|
|
13343
|
-
async function searchMessages(client, request) {
|
|
13344
|
-
if (!request.query?.trim() && !request.target && !request.sender) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass a query, or filter by target or sender." }));
|
|
13345
|
-
const result = await client.messages.search({
|
|
13346
|
-
...request.query?.trim() ? { q: request.query.trim() } : {},
|
|
13347
|
-
...request.target ? { channel: request.target } : {},
|
|
13348
|
-
...request.sender ? { sender: request.sender } : {},
|
|
13349
|
-
...request.sort ? { sort: request.sort } : {},
|
|
13350
|
-
...request.before ? { before: request.before } : {},
|
|
13351
|
-
...request.after ? { after: request.after } : {},
|
|
13352
|
-
...request.limit === void 0 ? {} : { limit: String(request.limit) },
|
|
13353
|
-
...request.offset === void 0 ? {} : { offset: String(request.offset) }
|
|
13354
|
-
});
|
|
13355
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13356
|
-
const data = result.data;
|
|
13357
|
-
const hasMore = typeof data.hasMore === "boolean" ? data.hasMore : null;
|
|
13358
|
-
const page = {
|
|
13359
|
-
query: request.query?.trim() ?? "",
|
|
13360
|
-
results: data.results,
|
|
13361
|
-
hasMore
|
|
13362
|
-
};
|
|
13363
|
-
const next = hasMore ? {
|
|
13364
|
-
kind: "next_search_page",
|
|
13365
|
-
args: {
|
|
13366
|
-
...request,
|
|
13367
|
-
offset: (request.offset ?? 0) + data.results.length
|
|
13368
|
-
},
|
|
13369
|
-
why: "More results exist; page on, or narrow the query."
|
|
13370
|
-
} : null;
|
|
13371
|
-
return {
|
|
13372
|
-
ok: true,
|
|
13373
|
-
state: data.results.length > 0 ? "results" : "empty",
|
|
13374
|
-
data: page,
|
|
13375
|
-
next,
|
|
13376
|
-
text: formatAgentSearchResults(page.query, {
|
|
13377
|
-
results: data.results,
|
|
13378
|
-
...hasMore === null ? {} : { hasMore }
|
|
13379
|
-
}, request.offset, request.sort, request.limit)
|
|
13380
|
-
};
|
|
13381
|
-
}
|
|
13382
|
-
async function resolveMessage(client, request) {
|
|
13383
|
-
if (!request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id is required." }));
|
|
13384
|
-
const result = await client.messages.resolve({ msgId: request.messageId });
|
|
13385
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13386
|
-
const message = projectRaftMessage(result.data.message);
|
|
13387
|
-
if (!message) return failureOutcome(opError("INVALID_RESPONSE", { message: "The resolved message carries no conversation identity." }));
|
|
13388
|
-
return {
|
|
13389
|
-
ok: true,
|
|
13390
|
-
state: "message",
|
|
13391
|
-
data: message,
|
|
13392
|
-
next: {
|
|
13393
|
-
kind: "read_target",
|
|
13394
|
-
command: `raft message read --target "${message.target}" --around ${message.shortId ?? request.messageId}`,
|
|
13395
|
-
args: {
|
|
13396
|
-
target: message.target,
|
|
13397
|
-
around: message.id ?? request.messageId
|
|
13398
|
-
},
|
|
13399
|
-
why: "Read the surrounding context before acting on one message."
|
|
13400
|
-
},
|
|
13401
|
-
text: formatAgentMessages([toAgentMessageLike(message.raw)])
|
|
13402
|
-
};
|
|
13403
|
-
}
|
|
13404
|
-
async function reactToMessage(client, request, action = "add") {
|
|
13405
|
-
const emoji = request.emoji?.trim();
|
|
13406
|
-
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." }));
|
|
13407
|
-
const params = { msgId: request.messageId };
|
|
13408
|
-
const result = action === "add" ? await client.messages.addReaction(params, { emoji }) : await client.messages.removeReaction(params, { emoji });
|
|
13409
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13410
|
-
const verb = action === "add" ? "added to" : "removed from";
|
|
13411
|
-
return {
|
|
13412
|
-
ok: true,
|
|
13413
|
-
state: action === "add" ? "added" : "removed",
|
|
13414
|
-
data: {
|
|
13415
|
-
messageId: request.messageId,
|
|
13416
|
-
emoji
|
|
13417
|
-
},
|
|
13418
|
-
next: null,
|
|
13419
|
-
text: `Reaction ${emoji} ${verb} message ${request.messageId.slice(0, 8)}.`
|
|
13420
|
-
};
|
|
13421
|
-
}
|
|
13422
|
-
//#endregion
|
|
13423
|
-
//#region ../shared/src/agentText/mentions.ts
|
|
13424
|
-
function normalizeAgentMentionAction(action) {
|
|
13425
|
-
if (action === "notify" || action === "notify_only") return "notify";
|
|
13426
|
-
if (action === "add" || action === "invite") return "add";
|
|
13427
|
-
return null;
|
|
13428
|
-
}
|
|
13429
|
-
function formatActionCommands(action) {
|
|
13430
|
-
const verbs = action.availableActions.map(normalizeAgentMentionAction).filter((verb) => verb !== null);
|
|
13431
|
-
return Array.from(new Set(verbs)).map((verb) => ` ${verb}: raft mention ${verb} ${action.resolutionId}`);
|
|
13432
|
-
}
|
|
13433
|
-
function formatAuthoredMentionToken(targetHandle) {
|
|
13434
|
-
return targetHandle.startsWith("@") ? targetHandle : `@${targetHandle}`;
|
|
13435
|
-
}
|
|
13436
|
-
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;
|
|
13437
|
-
/** Per-token mention recovery command line; null when the id is not a UUID (fail closed). */
|
|
13438
|
-
function formatAgentMentionNotifyRecoveryCommand(resolutionId) {
|
|
13439
|
-
return PENDING_MENTION_ACTION_ID_RE.test(resolutionId) ? `raft mention notify ${resolutionId}` : null;
|
|
13440
|
-
}
|
|
13441
|
-
function toAgentSenderPendingMentionAction(action) {
|
|
13442
|
-
return {
|
|
13443
|
-
resolutionId: action.resolutionId,
|
|
13444
|
-
messageId: action.messageId,
|
|
13445
|
-
targetHandle: formatAuthoredMentionToken(action.targetHandle),
|
|
13446
|
-
status: "not_queued",
|
|
13447
|
-
reason: "not_in_conversation",
|
|
13448
|
-
consequence: "This @mention did not notify anyone.",
|
|
13449
|
-
expiresAt: action.expiresAt ?? null,
|
|
13450
|
-
recoveryCommand: formatAgentMentionNotifyRecoveryCommand(action.resolutionId)
|
|
13451
|
-
};
|
|
13452
|
-
}
|
|
13453
|
-
function toAgentSenderUnresolvedMentionWarning(targetHandle) {
|
|
13454
|
-
return {
|
|
13455
|
-
targetHandle: formatAuthoredMentionToken(targetHandle),
|
|
13456
|
-
status: "not_queued",
|
|
13457
|
-
reason: "unknown_or_not_visible",
|
|
13458
|
-
consequence: "This @mention did not notify anyone.",
|
|
13459
|
-
expiresAt: null,
|
|
13460
|
-
recoveryCommand: null
|
|
13461
|
-
};
|
|
13462
|
-
}
|
|
13463
|
-
function formatPendingReason(reason) {
|
|
13464
|
-
if (reason === "not_member") return "not in the conversation at send time, so the @mention was not delivered";
|
|
13465
|
-
return reason;
|
|
13466
|
-
}
|
|
13467
|
-
function normalizeAgentPendingMentionActions(data) {
|
|
13468
|
-
const value = data;
|
|
13469
|
-
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) => ({
|
|
13470
|
-
resolutionId: String(item.resolutionId ?? item.id ?? ""),
|
|
13471
|
-
messageId: String(item.messageId ?? ""),
|
|
13472
|
-
targetType: String(item.targetType ?? "unknown"),
|
|
13473
|
-
targetHandle: String(item.targetHandle ?? ""),
|
|
13474
|
-
reason: String(item.reason ?? "Mention target was not notified at send time."),
|
|
13475
|
-
availableActions: Array.isArray(item.availableActions) ? item.availableActions.map(String) : [],
|
|
13476
|
-
expiresAt: typeof item.expiresAt === "string" ? item.expiresAt : null
|
|
13477
|
-
})).filter((item) => item.resolutionId.length > 0);
|
|
13478
|
-
}
|
|
13479
|
-
function normalizeAgentMentionActionResults(data) {
|
|
13480
|
-
const value = data;
|
|
13481
|
-
return (Array.isArray(value?.results) ? value.results : Array.isArray(value?.actionResults) ? value.actionResults : []).filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
|
|
13482
|
-
resolutionId: String(item.resolutionId ?? item.id ?? ""),
|
|
13483
|
-
status: String(item.status ?? "not_found"),
|
|
13484
|
-
action: typeof item.action === "string" ? normalizeAgentMentionAction(item.action) : null,
|
|
13485
|
-
messageId: typeof item.messageId === "string" ? item.messageId : null,
|
|
13486
|
-
channelId: typeof item.channelId === "string" ? item.channelId : null,
|
|
13487
|
-
targetType: typeof item.targetType === "string" ? item.targetType : null,
|
|
13488
|
-
targetId: typeof item.targetId === "string" ? item.targetId : null,
|
|
13489
|
-
targetHandle: typeof item.targetHandle === "string" ? item.targetHandle : null,
|
|
13490
|
-
message: typeof item.message === "string" ? item.message : null,
|
|
13491
|
-
reason: typeof item.reason === "string" ? item.reason : null,
|
|
13492
|
-
dedupedResolutionIds: Array.isArray(item.dedupedResolutionIds) ? item.dedupedResolutionIds.filter((id) => typeof id === "string" && id.length > 0) : []
|
|
13493
|
-
})).filter((item) => item.resolutionId.length > 0);
|
|
13494
|
-
}
|
|
13495
|
-
const PENDING_DEFAULT_LIMIT = 50;
|
|
13496
|
-
function pendingPageVerdict(hasMore, limit, shown) {
|
|
13497
|
-
const asked = limit === void 0 ? "server default 50" : `--limit ${limit}`;
|
|
13498
|
-
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`;
|
|
13499
|
-
if (hasMore === false) return `shown ${shown}, ${asked} — truncated=false · this is the complete list`;
|
|
13500
|
-
return `shown ${shown}, ${asked} — truncated=unknown · server did not report has_more, so completeness is NOT asserted`;
|
|
13501
|
-
}
|
|
13502
|
-
/** Undelivered-mentions partial result (send) or the pending list. */
|
|
13503
|
-
function formatAgentPendingMentionActions(actions, opts = {}) {
|
|
13504
|
-
const unresolvedMentionHandles = opts.source === "send" ? Array.from(new Set(opts.unresolvedMentionHandles ?? [])) : [];
|
|
13505
|
-
const pageNote = opts.source === "pending" ? pendingPageVerdict(opts.hasMore, opts.limit, actions.length) : "";
|
|
13506
|
-
if (actions.length === 0 && unresolvedMentionHandles.length === 0) return opts.source === "pending" ? `Pending mention actions\n\nNo pending mention actions. (${pageNote})\n` : "";
|
|
13507
|
-
if (opts.source === "send") {
|
|
13508
|
-
const lines = [
|
|
13509
|
-
"Undelivered mentions — partial result",
|
|
13510
|
-
"Message effect: status=queued. Queue acceptance is the only message proof.",
|
|
13511
|
-
"Do not rerun `raft message send`; the message is already queued and a retry could duplicate it.",
|
|
13512
|
-
"Each row below is bound to the literal @token from your message.",
|
|
13513
|
-
"For a literal name rather than a recipient, wrap the @handle in inline or fenced code.",
|
|
13514
|
-
""
|
|
13515
|
-
];
|
|
13516
|
-
for (const rawAction of actions) {
|
|
13517
|
-
const action = toAgentSenderPendingMentionAction(rawAction);
|
|
13518
|
-
lines.push(`- ${action.targetHandle} — status=${action.status}`);
|
|
13519
|
-
lines.push(` reason: ${action.reason}`);
|
|
13520
|
-
lines.push(` consequence: ${action.consequence}`);
|
|
13521
|
-
lines.push(` pending action: ${action.recoveryCommand ? action.resolutionId : "[invalid pending action id]"}`);
|
|
13522
|
-
if (action.messageId) lines.push(` message: ${action.messageId}`);
|
|
13523
|
-
lines.push(` expires: ${action.expiresAt ?? "unknown"}`);
|
|
13524
|
-
if (action.recoveryCommand) {
|
|
13525
|
-
lines.push(` recovery: ${action.recoveryCommand}`);
|
|
13526
|
-
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.");
|
|
13527
|
-
lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
|
|
13528
|
-
} else lines.push(" recovery: unavailable because the pending action id is invalid; inspect `raft mention pending` without resending the message.");
|
|
13529
|
-
}
|
|
13530
|
-
for (const rawHandle of unresolvedMentionHandles) {
|
|
13531
|
-
const warning = toAgentSenderUnresolvedMentionWarning(rawHandle);
|
|
13532
|
-
lines.push(`- ${warning.targetHandle} — status=${warning.status}`);
|
|
13533
|
-
lines.push(` reason: ${warning.reason}`);
|
|
13534
|
-
lines.push(` consequence: ${warning.consequence}`);
|
|
13535
|
-
lines.push(" pending action: none; no visible target resolved for this token");
|
|
13536
|
-
lines.push(" expires: n/a");
|
|
13537
|
-
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.");
|
|
13538
|
-
}
|
|
13539
|
-
return `${lines.join("\n")}\n`;
|
|
13540
|
-
}
|
|
13541
|
-
const lines = [
|
|
13542
|
-
"Pending mention actions",
|
|
13543
|
-
`(${pageNote})`,
|
|
13544
|
-
""
|
|
13545
|
-
];
|
|
13546
|
-
for (const action of actions) {
|
|
13547
|
-
const target = action.targetHandle ? `${action.targetHandle} (${action.targetType})` : action.targetType;
|
|
13548
|
-
lines.push(`- ${action.resolutionId} — ${target}`);
|
|
13549
|
-
if (action.messageId) lines.push(` message: ${action.messageId}`);
|
|
13550
|
-
lines.push(` reason: ${formatPendingReason(action.reason)}`);
|
|
13551
|
-
if (action.expiresAt) lines.push(` expires: ${action.expiresAt}`);
|
|
13552
|
-
const commands = formatActionCommands(action);
|
|
13553
|
-
if (commands.length > 0) {
|
|
13554
|
-
lines.push(" recovery commands:");
|
|
13555
|
-
lines.push(...commands);
|
|
13556
|
-
if (commands.some((command) => command.includes(" mention notify "))) lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
|
|
13557
|
-
}
|
|
13558
|
-
}
|
|
13559
|
-
return `${lines.join("\n")}\n`;
|
|
13560
|
-
}
|
|
13561
|
-
function formatMentionActionDetail(result) {
|
|
13562
|
-
const detail = result.message ?? result.reason ?? null;
|
|
13563
|
-
if (result.status === "dropped") return detail ? `not delivered: ${detail}` : "not delivered";
|
|
13564
|
-
return detail;
|
|
13565
|
-
}
|
|
13566
|
-
/** notify/add action outcome rows. */
|
|
13567
|
-
function formatAgentMentionActionResults(action, results) {
|
|
13568
|
-
const lines = [`Mention ${action} results`, ""];
|
|
13569
|
-
if (results.length === 0) {
|
|
13570
|
-
lines.push("No result rows returned.");
|
|
13571
|
-
return `${lines.join("\n")}\n`;
|
|
13572
|
-
}
|
|
13573
|
-
for (const result of results) {
|
|
13574
|
-
const target = result.targetHandle ? ` ${result.targetHandle}` : "";
|
|
13575
|
-
const detail = formatMentionActionDetail(result);
|
|
13576
|
-
const suffix = detail ? ` — ${detail}` : "";
|
|
13577
|
-
lines.push(`- ${result.resolutionId}${target}: ${result.status}${suffix}`);
|
|
13578
|
-
if (result.dedupedResolutionIds && result.dedupedResolutionIds.length > 1) lines.push(` deduped: ${result.dedupedResolutionIds.join(", ")}`);
|
|
13579
|
-
}
|
|
13580
|
-
if (action === "notify" && results.some((result) => result.status === "queued" && result.reason !== "already_queued")) lines.push("", `Recipient guidance: ${formatAgentReplyAffordance({ non_member_mention: true })}`);
|
|
13581
|
-
return `${lines.join("\n")}\n`;
|
|
13582
|
-
}
|
|
13583
|
-
function describeDelivery(row) {
|
|
13584
|
-
switch (row.outcome) {
|
|
13585
|
-
case "delivered": return "delivered";
|
|
13586
|
-
case "pending": return "still in flight — do not conclude yet";
|
|
13587
|
-
case "unknown": return "UNKNOWN — our instrument could not see this one; re-check, do not conclude";
|
|
13588
|
-
case "lost": switch (row.reasonCategory) {
|
|
13589
|
-
case "quota": return "LOST — target is rate/quota limited; it will not arrive, re-route or retry later";
|
|
13590
|
-
case "runtime_error": return "LOST — target's runtime rejected delivery; re-route";
|
|
13591
|
-
case "not_launched": return "LOST — target was not running; re-route or start it";
|
|
13592
|
-
case "unclassified": return "LOST — cause is outside the seat-availability set; re-route";
|
|
13593
|
-
default: return "LOST — server sent no category for this row (malformed); treat the cause as unknown";
|
|
13594
|
-
}
|
|
13595
|
-
}
|
|
13596
|
-
}
|
|
13597
|
-
/** Per-target mention delivery outcome for a message the bound agent sent. */
|
|
13598
|
-
function formatAgentSenderMentionDeliveries(messageId, deliveries) {
|
|
13599
|
-
if (deliveries.length === 0) return `Message ${messageId}\n\nNo mention deliveries recorded for this message.\n`;
|
|
13600
|
-
const lines = [`Message ${messageId} — mention delivery per target`, ""];
|
|
13601
|
-
for (const row of deliveries) lines.push(` ${row.targetHandle || "(unknown target)"}: ${describeDelivery(row)}`);
|
|
13602
|
-
return `${lines.join("\n")}\n`;
|
|
13603
|
-
}
|
|
13604
|
-
//#endregion
|
|
13605
|
-
//#region ../shared/src/agentOps/mentions.ts
|
|
13606
|
-
async function pendingMentionActions(client, request = {}) {
|
|
13607
|
-
const result = await client.mentions.pendingActions(request.limit === void 0 ? {} : { limit: String(request.limit) });
|
|
13608
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13609
|
-
const actions = normalizeAgentPendingMentionActions(result.data);
|
|
13610
|
-
const hasMore = typeof result.data.has_more === "boolean" ? result.data.has_more : null;
|
|
13611
|
-
const first = actions.find((a) => a.availableActions.length > 0);
|
|
13612
|
-
const next = first ? {
|
|
13613
|
-
kind: "resolve_mention",
|
|
13614
|
-
command: `raft mention notify ${first.resolutionId}`,
|
|
13615
|
-
args: {
|
|
13616
|
-
action: "notify",
|
|
13617
|
-
resolutionIds: [first.resolutionId]
|
|
13618
|
-
},
|
|
13619
|
-
why: "This @mention reached nobody at send time; notify the target (or add them) so the message is seen."
|
|
13620
|
-
} : null;
|
|
13621
|
-
return {
|
|
13622
|
-
ok: true,
|
|
13623
|
-
state: actions.length > 0 ? "pending" : "empty",
|
|
13624
|
-
data: {
|
|
13625
|
-
actions,
|
|
13626
|
-
hasMore,
|
|
13627
|
-
limit: request.limit
|
|
13628
|
-
},
|
|
13629
|
-
next,
|
|
13630
|
-
text: formatAgentPendingMentionActions(actions, {
|
|
13631
|
-
source: "pending",
|
|
13632
|
-
...hasMore === null ? {} : { hasMore },
|
|
13633
|
-
limit: request.limit
|
|
13634
|
-
})
|
|
13635
|
-
};
|
|
13636
|
-
}
|
|
13637
|
-
async function executeMentionAction(client, request) {
|
|
13638
|
-
const ids = (request.resolutionIds ?? []).map((id) => id.trim()).filter(Boolean);
|
|
13639
|
-
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." }));
|
|
13640
|
-
const result = await client.mentions.executeAction({
|
|
13641
|
-
action: request.action,
|
|
13642
|
-
resolutionIds: ids
|
|
13643
|
-
});
|
|
13644
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13645
|
-
const results = normalizeAgentMentionActionResults(result.data);
|
|
13646
|
-
return {
|
|
13647
|
-
ok: true,
|
|
13648
|
-
state: "executed",
|
|
13649
|
-
data: {
|
|
13650
|
-
action: request.action,
|
|
13651
|
-
results
|
|
13652
|
-
},
|
|
13653
|
-
next: null,
|
|
13654
|
-
text: formatAgentMentionActionResults(request.action, results)
|
|
13655
|
-
};
|
|
13656
|
-
}
|
|
13657
|
-
async function senderMentionDeliveries(client, request) {
|
|
13658
|
-
if (!request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id is required." }));
|
|
13659
|
-
const result = await client.mentions.senderDeliveries({ messageId: request.messageId });
|
|
13660
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13661
|
-
const data = result.data;
|
|
13662
|
-
return {
|
|
13663
|
-
ok: true,
|
|
13664
|
-
state: data.deliveries.length > 0 ? "deliveries" : "empty",
|
|
13665
|
-
data,
|
|
13666
|
-
next: null,
|
|
13667
|
-
text: formatAgentSenderMentionDeliveries(data.messageId, data.deliveries)
|
|
13668
|
-
};
|
|
13669
|
-
}
|
|
13670
|
-
//#endregion
|
|
13671
|
-
//#region ../shared/src/agentText/knowledge.ts
|
|
13672
|
-
function formatMatchReason(result) {
|
|
13673
|
-
const parts = [];
|
|
13674
|
-
for (const { term, matched } of result.correctedTerms ?? []) parts.push(`${term} → ${matched} (typo)`);
|
|
13675
|
-
for (const { from, to } of result.expandedTerms ?? []) parts.push(`${from} → ${to} (concept)`);
|
|
13676
|
-
return parts.length > 0 ? ` matched: ${parts.join(", ")}` : "";
|
|
13677
|
-
}
|
|
13678
|
-
function formatAgentKnowledgeSearchResults(results) {
|
|
13679
|
-
return results.map((result, index) => {
|
|
13680
|
-
const body = result.firstScreen.trim().split(/\r?\n/).filter(Boolean).map((line) => ` ${line}`).join("\n");
|
|
13681
|
-
const reason = formatMatchReason(result);
|
|
13682
|
-
return `${index + 1}. ${result.slug} — ${result.title}${reason ? `\n${reason}` : ""}${body ? `\n${body}` : ""}`;
|
|
13683
|
-
}).join("\n\n") + "\n";
|
|
13684
|
-
}
|
|
13685
|
-
function formatAgentKnowledgeStdout(content) {
|
|
13686
|
-
return content.endsWith("\n") ? content : `${content}\n`;
|
|
13687
|
-
}
|
|
13688
|
-
//#endregion
|
|
13689
|
-
//#region ../shared/src/agentOps/manual.ts
|
|
13690
|
-
function requireContext(context) {
|
|
13691
|
-
if (!context?.intent?.trim() || !context?.reason?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Manual calls need a short `intent` and `reason`." }));
|
|
13692
|
-
return null;
|
|
13693
|
-
}
|
|
13694
|
-
async function getManualTopic(client, request) {
|
|
13695
|
-
const invalid = requireContext(request);
|
|
13696
|
-
if (invalid) return invalid;
|
|
13697
|
-
if (!request.topic?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A topic id is required." }));
|
|
13698
|
-
const result = await client.knowledge.get({
|
|
13699
|
-
topic: request.topic.trim(),
|
|
13700
|
-
intent: request.intent.trim(),
|
|
13701
|
-
reason: request.reason.trim()
|
|
13702
|
-
});
|
|
13703
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13704
|
-
return {
|
|
13705
|
-
ok: true,
|
|
13706
|
-
state: "topic",
|
|
13707
|
-
data: result.data,
|
|
13708
|
-
next: null,
|
|
13709
|
-
text: formatAgentKnowledgeStdout(result.data.content)
|
|
13710
|
-
};
|
|
13711
|
-
}
|
|
13712
|
-
async function searchManual(client, request) {
|
|
13713
|
-
const invalid = requireContext(request);
|
|
13714
|
-
if (invalid) return invalid;
|
|
13715
|
-
if (!request.query?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "Search keywords are required." }));
|
|
13716
|
-
const result = await client.knowledge.search({
|
|
13717
|
-
query: request.query.trim(),
|
|
13718
|
-
...request.scope ? { scope: request.scope } : {},
|
|
13719
|
-
intent: request.intent.trim(),
|
|
13720
|
-
reason: request.reason.trim()
|
|
13721
|
-
});
|
|
13722
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13723
|
-
const data = result.data;
|
|
13724
|
-
const results = data.results;
|
|
13725
|
-
const first = results[0];
|
|
13726
|
-
const next = first ? {
|
|
13727
|
-
kind: "read_manual_topic",
|
|
13728
|
-
command: `raft manual get ${first.slug} --intent "…" --reason "…"`,
|
|
13729
|
-
args: { topic: first.slug },
|
|
13730
|
-
why: "Open the best-matching topic."
|
|
13731
|
-
} : null;
|
|
13732
|
-
return {
|
|
13733
|
-
ok: true,
|
|
13734
|
-
state: results.length > 0 ? "results" : "empty",
|
|
13735
|
-
data,
|
|
13736
|
-
next,
|
|
13737
|
-
text: results.length > 0 ? formatAgentKnowledgeSearchResults(results) : "No Manual topics matched. Retry with different keywords, keeping your intent and reason.\n"
|
|
13738
|
-
};
|
|
13739
|
-
}
|
|
13740
|
-
//#endregion
|
|
13741
|
-
//#region ../shared/src/agentOps/state.ts
|
|
13742
|
-
const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
|
|
13743
|
-
const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
13744
|
-
const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
13745
|
-
function isNullableCursor(value) {
|
|
13746
|
-
return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
13747
|
-
}
|
|
13748
|
-
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
13749
|
-
function parseRaftState(value) {
|
|
13750
|
-
if (!value || typeof value !== "object") return null;
|
|
13751
|
-
const v = value;
|
|
13752
|
-
if (v.schema !== "raft-sdk-state.v1") return null;
|
|
13753
|
-
if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
|
|
13754
|
-
if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
|
|
13755
|
-
const frontier = v.frontier;
|
|
13756
|
-
const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
|
|
13757
|
-
const r = c;
|
|
13758
|
-
return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
|
|
13759
|
-
}) : [];
|
|
13760
|
-
return {
|
|
13761
|
-
schema: RAFT_STATE_SCHEMA,
|
|
13762
|
-
version: v.version,
|
|
13763
|
-
cursor: v.cursor ?? null,
|
|
13764
|
-
pendingCursor: v.pendingCursor ?? null,
|
|
13765
|
-
frontier: frontier && frontier.version === 1 ? frontier : {
|
|
13766
|
-
version: 1,
|
|
13767
|
-
targets: {},
|
|
13768
|
-
aliases: {}
|
|
13769
|
-
},
|
|
13770
|
-
continuations
|
|
13771
|
-
};
|
|
13772
|
-
}
|
|
13773
|
-
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
13774
|
-
async function hashRaftSendContent(target, content, attachmentIds = []) {
|
|
13775
|
-
const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
|
|
13776
|
-
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13777
|
-
}
|
|
13778
|
-
/**
|
|
13779
|
-
* One client's view of the persisted state: loads once, holds the working
|
|
13780
|
-
* copy, saves on request. Also used without a store (memory only), so the
|
|
13781
|
-
* pending/committed cursor flow behaves the same with or without persistence.
|
|
13782
|
-
*/
|
|
13783
|
-
var RaftStateSession = class {
|
|
13784
|
-
store;
|
|
13785
|
-
onError;
|
|
13786
|
-
frontier;
|
|
13787
|
-
cursor = null;
|
|
13788
|
-
pendingCursor = null;
|
|
13789
|
-
continuations = [];
|
|
13790
|
-
loadedVersion = void 0;
|
|
13791
|
-
loading = null;
|
|
13792
|
-
dirty = false;
|
|
13793
|
-
constructor(store, frontier, onError) {
|
|
13794
|
-
this.store = store;
|
|
13795
|
-
this.onError = onError;
|
|
13796
|
-
this.frontier = frontier;
|
|
13797
|
-
}
|
|
13798
|
-
get persistent() {
|
|
13799
|
-
return this.store !== void 0;
|
|
13800
|
-
}
|
|
13801
|
-
/** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
|
|
13802
|
-
get version() {
|
|
13803
|
-
return this.loadedVersion;
|
|
13804
|
-
}
|
|
13805
|
-
/** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
|
|
13806
|
-
ensureLoaded() {
|
|
13807
|
-
if (!this.store) return Promise.resolve();
|
|
13808
|
-
this.loading ??= (async () => {
|
|
13809
|
-
let raw = null;
|
|
13810
|
-
try {
|
|
13811
|
-
raw = await this.store.load();
|
|
13812
|
-
} catch (error) {
|
|
13813
|
-
this.onError?.(error, {
|
|
13814
|
-
phase: "load",
|
|
13815
|
-
version: void 0
|
|
13816
|
-
});
|
|
13817
|
-
return;
|
|
13818
|
-
}
|
|
13819
|
-
const state = parseRaftState(raw);
|
|
13820
|
-
if (!state) return;
|
|
13821
|
-
this.loadedVersion = state.version;
|
|
13822
|
-
this.cursor = state.cursor;
|
|
13823
|
-
this.pendingCursor = state.pendingCursor;
|
|
13824
|
-
this.frontier.absorb(state.frontier);
|
|
13825
|
-
this.continuations = state.continuations ?? [];
|
|
13826
|
-
})();
|
|
13827
|
-
return this.loading;
|
|
13828
|
-
}
|
|
13829
|
-
markDirty() {
|
|
13830
|
-
this.dirty = true;
|
|
13831
|
-
}
|
|
13832
|
-
/** Record the cursor of a batch just returned. Acknowledges nothing. */
|
|
13833
|
-
setPending(cursor) {
|
|
13834
|
-
if (cursor === null || cursor === this.pendingCursor) return;
|
|
13835
|
-
this.pendingCursor = cursor;
|
|
13836
|
-
this.dirty = true;
|
|
13837
|
-
}
|
|
13838
|
-
/**
|
|
13839
|
-
* Promote a cursor to committed. With no argument, commits `pendingCursor`.
|
|
13840
|
-
* Returns the committed cursor, or null when there was nothing to commit.
|
|
13841
|
-
* Never moves the committed cursor backwards.
|
|
13842
|
-
*/
|
|
13843
|
-
commit(cursor) {
|
|
13844
|
-
const next = cursor === void 0 ? this.pendingCursor : cursor;
|
|
13845
|
-
if (next === null || next === void 0) return null;
|
|
13846
|
-
if (this.cursor === null || next > this.cursor) this.cursor = next;
|
|
13847
|
-
if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
|
|
13848
|
-
this.dirty = true;
|
|
13849
|
-
return this.cursor;
|
|
13850
|
-
}
|
|
13851
|
-
findContinuation(target, contentHash) {
|
|
13852
|
-
return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
|
|
13853
|
-
}
|
|
13854
|
-
rememberContinuation(entry) {
|
|
13855
|
-
this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
|
|
13856
|
-
this.continuations.push(entry);
|
|
13857
|
-
const perTarget = this.continuations.filter((c) => c.target === entry.target);
|
|
13858
|
-
for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
|
|
13859
|
-
if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
|
|
13860
|
-
this.dirty = true;
|
|
13861
|
-
}
|
|
13862
|
-
forgetContinuation(target, contentHash) {
|
|
13863
|
-
const before = this.continuations.length;
|
|
13864
|
-
this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
|
|
13865
|
-
if (this.continuations.length !== before) this.dirty = true;
|
|
13866
|
-
}
|
|
13867
|
-
snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
|
|
13868
|
-
return {
|
|
13869
|
-
schema: RAFT_STATE_SCHEMA,
|
|
13870
|
-
version: nextVersion,
|
|
13871
|
-
cursor: this.cursor,
|
|
13872
|
-
pendingCursor: this.pendingCursor,
|
|
13873
|
-
frontier: this.frontier.snapshot(),
|
|
13874
|
-
continuations: [...this.continuations]
|
|
13875
|
-
};
|
|
13876
|
-
}
|
|
13877
|
-
/**
|
|
13878
|
-
* Save if anything changed (or `force`). One attempt; a failure is reported
|
|
13879
|
-
* and swallowed. Returns whether the save succeeded (true when nothing to save).
|
|
13880
|
-
*/
|
|
13881
|
-
async save(options = {}) {
|
|
13882
|
-
if (!this.store) return true;
|
|
13883
|
-
if (!this.dirty && !options.force) return true;
|
|
13884
|
-
const expectedVersion = this.loadedVersion;
|
|
13885
|
-
const state = this.snapshot();
|
|
13886
|
-
try {
|
|
13887
|
-
await this.store.save(state, { expectedVersion });
|
|
13888
|
-
} catch (error) {
|
|
13889
|
-
this.onError?.(error, {
|
|
13890
|
-
phase: "save",
|
|
13891
|
-
version: expectedVersion
|
|
13892
|
-
});
|
|
13893
|
-
return false;
|
|
13894
|
-
}
|
|
13895
|
-
this.loadedVersion = state.version;
|
|
13896
|
-
this.dirty = false;
|
|
13897
|
-
return true;
|
|
13898
|
-
}
|
|
13899
|
-
};
|
|
13900
|
-
//#endregion
|
|
13901
|
-
//#region ../shared/src/agentOps/actions.ts
|
|
13902
|
-
/** Text is the CLI's `raft action prepare` output. */
|
|
13903
|
-
function formatAgentActionCardPosted(target, messageId) {
|
|
13904
|
-
const shortId = messageId ? messageId.slice(0, 8) : null;
|
|
13905
|
-
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`;
|
|
13906
|
-
}
|
|
13907
|
-
async function prepareActionCard(client, request) {
|
|
13908
|
-
if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
|
|
13909
|
-
const result = await client.actions.prepare(request);
|
|
13910
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
13911
|
-
const card = {
|
|
13912
|
-
target: request.target,
|
|
13913
|
-
messageId: result.data.messageId
|
|
13914
|
-
};
|
|
13915
|
-
return {
|
|
13916
|
-
ok: true,
|
|
13917
|
-
state: "prepared",
|
|
13918
|
-
data: card,
|
|
13919
|
-
next: {
|
|
13920
|
-
kind: "await_confirmation",
|
|
13921
|
-
command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
|
|
13922
|
-
args: {
|
|
13923
|
-
target: request.target,
|
|
13924
|
-
messageId: card.messageId
|
|
13925
|
-
},
|
|
13926
|
-
why: "A human must click the card to commit it; its outcome arrives in your inbox."
|
|
13927
|
-
},
|
|
13928
|
-
text: formatAgentActionCardPosted(request.target, card.messageId)
|
|
13929
|
-
};
|
|
13930
|
-
}
|
|
13931
|
-
//#endregion
|
|
13932
|
-
//#region src/raft.ts
|
|
13933
|
-
function createRaft(options) {
|
|
13934
|
-
const serverUrl = requireServerUrl(options.serverUrl);
|
|
13935
|
-
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
13936
|
-
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
13937
|
-
const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
|
|
13938
|
-
/** Load before, run, then save if the operation succeeded and changed the state. */
|
|
13939
|
-
const withState = async (run) => {
|
|
13940
|
-
await session.ensureLoaded();
|
|
13941
|
-
const outcome = await run();
|
|
13942
|
-
if (outcome.ok) await session.save();
|
|
13943
|
-
return outcome;
|
|
13944
|
-
};
|
|
13945
|
-
const api = createRaftRouteClient({
|
|
13946
|
-
serverUrl,
|
|
13947
|
-
fetch: options.fetch,
|
|
13948
|
-
headers: options.headers,
|
|
13949
|
-
authorization,
|
|
13950
|
-
readAttempts: options.retry?.attempts,
|
|
13951
|
-
beforeRequest: options.throttle?.beforeRequest
|
|
13952
|
-
});
|
|
13953
|
-
const routes = raftRoutesFromClient(api);
|
|
13954
|
-
const inboxApi = createAgentApiClient({ fetch: {
|
|
13955
|
-
baseUrl: serverUrl,
|
|
13956
|
-
fetch: (input, init) => (options.fetch ?? fetch)(input, {
|
|
13957
|
-
...init,
|
|
13958
|
-
cache: "no-store",
|
|
13959
|
-
redirect: "error"
|
|
13960
|
-
}),
|
|
13961
|
-
headers: Object.fromEntries(new Headers(options.headers)),
|
|
13962
|
-
retry: { attempts: 1 },
|
|
13963
|
-
throttle: options.throttle,
|
|
13964
|
-
auth: { authorization }
|
|
13965
|
-
} });
|
|
13966
|
-
/**
|
|
13967
|
-
* Send through the state session: a resend of the same logical message
|
|
13968
|
-
* (same target, content, attachments) reuses the held send's idempotency key;
|
|
13969
|
-
* a hold is remembered; a successful send clears it.
|
|
13970
|
-
*/
|
|
13971
|
-
const sendWithState = (request) => withState(async () => {
|
|
13972
|
-
const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
|
|
13973
|
-
const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
|
|
13974
|
-
const outcome = await sendMessage(api, pending ? {
|
|
13975
|
-
...request,
|
|
13976
|
-
idempotencyKey: pending.idempotencyKey
|
|
13977
|
-
} : request, frontier);
|
|
13978
|
-
if (outcome.ok && outcome.state === "held") session.rememberContinuation({
|
|
13979
|
-
target: request.target,
|
|
13980
|
-
idempotencyKey: outcome.data.idempotencyKey,
|
|
13981
|
-
contentHash,
|
|
13982
|
-
heldAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13983
|
-
});
|
|
13984
|
-
else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
|
|
13985
|
-
return outcome;
|
|
13986
|
-
});
|
|
13987
|
-
return {
|
|
13988
|
-
identity: { whoami: () => getRaftContext(api) },
|
|
13989
|
-
wake: {
|
|
13990
|
-
verifyNotice: (input) => verifyInboxNotice(input),
|
|
13991
|
-
webhook: {
|
|
13992
|
-
status: () => webhookStatus(api),
|
|
13993
|
-
register: (request) => registerWebhook(api, request),
|
|
13994
|
-
unregister: () => unregisterWebhook(api)
|
|
13995
|
-
}
|
|
13996
|
-
},
|
|
13997
|
-
inbox: {
|
|
13998
|
-
check: (request = {}) => withState(async () => {
|
|
13999
|
-
const since = request.since ?? session.cursor ?? void 0;
|
|
14000
|
-
const outcome = await checkInbox(inboxApi, {
|
|
14001
|
-
...request,
|
|
14002
|
-
...since === void 0 ? {} : { since }
|
|
14003
|
-
}, frontier);
|
|
14004
|
-
if (outcome.ok) {
|
|
14005
|
-
session.markDirty();
|
|
14006
|
-
if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
|
|
14007
|
-
if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
|
|
14008
|
-
}
|
|
14009
|
-
return outcome;
|
|
14010
|
-
}),
|
|
14011
|
-
commit: async (target) => {
|
|
14012
|
-
await session.ensureLoaded();
|
|
14013
|
-
const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
|
|
14014
|
-
return {
|
|
14015
|
-
cursor,
|
|
14016
|
-
saved: cursor === null ? true : await session.save()
|
|
14017
|
-
};
|
|
14018
|
-
},
|
|
14019
|
-
drain: (request = {}) => (async function* () {
|
|
14020
|
-
await session.ensureLoaded();
|
|
14021
|
-
const since = request.since ?? session.cursor ?? void 0;
|
|
14022
|
-
return yield* drainInbox(inboxApi, {
|
|
14023
|
-
...request,
|
|
14024
|
-
...since === void 0 ? {} : { since }
|
|
14025
|
-
}, frontier, async (sent, batch) => {
|
|
14026
|
-
if (sent !== null) session.commit(sent);
|
|
14027
|
-
if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
|
|
14028
|
-
session.markDirty();
|
|
14029
|
-
await session.save();
|
|
14030
|
-
});
|
|
14031
|
-
})(),
|
|
14032
|
-
list: (request) => listInbox(api, request)
|
|
14033
|
-
},
|
|
14034
|
-
messages: {
|
|
14035
|
-
read: (request) => withState(async () => {
|
|
14036
|
-
const outcome = await readHistory(api, request, frontier);
|
|
14037
|
-
if (outcome.ok) session.markDirty();
|
|
14038
|
-
return outcome;
|
|
14039
|
-
}),
|
|
14040
|
-
send: (request) => sendWithState(request),
|
|
14041
|
-
reply: (message, request) => sendWithState({
|
|
14042
|
-
...request,
|
|
14043
|
-
target: message.target
|
|
14044
|
-
}),
|
|
14045
|
-
search: (request) => searchMessages(api, request),
|
|
14046
|
-
resolve: (request) => resolveMessage(api, request),
|
|
14047
|
-
react: (request) => reactToMessage(api, request, "add"),
|
|
14048
|
-
unreact: (request) => reactToMessage(api, request, "remove")
|
|
14049
|
-
},
|
|
14050
|
-
attachments: {
|
|
14051
|
-
upload: (request) => uploadAttachment(api, {
|
|
14052
|
-
serverUrl,
|
|
14053
|
-
fetch: options.fetch ?? fetch,
|
|
14054
|
-
headers: Object.fromEntries(new Headers(options.headers)),
|
|
14055
|
-
authorization
|
|
14056
|
-
}, request),
|
|
14057
|
-
download: (request) => downloadAttachment(api, request),
|
|
14058
|
-
comments: (request) => attachmentComments(api, request)
|
|
14059
|
-
},
|
|
14060
|
-
mentions: {
|
|
14061
|
-
pending: (request) => pendingMentionActions(api, request),
|
|
14062
|
-
execute: (request) => executeMentionAction(api, request),
|
|
14063
|
-
deliveries: (request) => senderMentionDeliveries(api, request)
|
|
14064
|
-
},
|
|
14065
|
-
actions: { prepare: (request) => prepareActionCard(api, request) },
|
|
14066
|
-
manual: {
|
|
14067
|
-
get: (request) => getManualTopic(api, request),
|
|
14068
|
-
search: (request) => searchManual(api, request)
|
|
14069
|
-
},
|
|
14070
|
-
tasks: {
|
|
14071
|
-
claim: (request) => claimTasks(api, request),
|
|
14072
|
-
list: (request) => listTasks(api, request),
|
|
14073
|
-
create: (request) => createTasks(api, request),
|
|
14074
|
-
unclaim: (request) => unclaimTask(api, request),
|
|
14075
|
-
assign: (request) => assignTask(api, request),
|
|
14076
|
-
updateStatus: (request) => updateTaskStatus(api, request),
|
|
14077
|
-
amend: (request) => amendTask(api, request),
|
|
14078
|
-
history: (request) => taskHistory(api, request),
|
|
14079
|
-
convert: (request) => convertMessageToTask(api, request),
|
|
14080
|
-
delete: (request) => deleteTask(api, request)
|
|
14081
|
-
},
|
|
14082
|
-
channels: {
|
|
14083
|
-
join: (request) => joinChannel(api, request),
|
|
14084
|
-
leave: (request) => leaveChannel(api, request),
|
|
14085
|
-
mute: (request) => muteChannel(api, request),
|
|
14086
|
-
unmute: (request) => unmuteChannel(api, request),
|
|
14087
|
-
members: (request) => channelMembers(api, request)
|
|
14088
|
-
},
|
|
14089
|
-
threads: {
|
|
14090
|
-
list: () => listThreads(api),
|
|
14091
|
-
unfollow: (request) => unfollowThread(api, request)
|
|
14092
|
-
},
|
|
14093
|
-
server: { info: (request) => serverInfo(api, request) },
|
|
14094
|
-
profile: {
|
|
14095
|
-
show: (request) => showProfile(api, request),
|
|
14096
|
-
update: (request) => updateProfile(api, request)
|
|
11811
|
+
messages: {
|
|
11812
|
+
read: (request) => readHistory(routes, request, frontier),
|
|
11813
|
+
send: (request) => sendMessage(routes, request, frontier),
|
|
11814
|
+
reply: (message, request) => replyTo(routes, message, request, frontier)
|
|
14097
11815
|
},
|
|
11816
|
+
tasks: { claim: (request) => claimTasks(routes, request) },
|
|
14098
11817
|
frontier,
|
|
14099
|
-
state: {
|
|
14100
|
-
save: async () => {
|
|
14101
|
-
await session.ensureLoaded();
|
|
14102
|
-
session.markDirty();
|
|
14103
|
-
return session.save();
|
|
14104
|
-
},
|
|
14105
|
-
load: () => session.ensureLoaded(),
|
|
14106
|
-
snapshot: () => session.snapshot(session.version ?? 0)
|
|
14107
|
-
},
|
|
14108
11818
|
routes
|
|
14109
11819
|
};
|
|
14110
11820
|
}
|
|
@@ -14112,9 +11822,6 @@ function createRaft(options) {
|
|
|
14112
11822
|
exports.RAFT_INBOX_NOTICE_SCHEMA = RAFT_INBOX_NOTICE_SCHEMA;
|
|
14113
11823
|
exports.RAFT_NOTICE_DELIVERY_ID_HEADER = RAFT_NOTICE_DELIVERY_ID_HEADER;
|
|
14114
11824
|
exports.RAFT_NOTICE_SIGNATURE_HEADER = RAFT_NOTICE_SIGNATURE_HEADER;
|
|
14115
|
-
exports.RAFT_STATE_CONTINUATIONS_PER_TARGET = RAFT_STATE_CONTINUATIONS_PER_TARGET;
|
|
14116
|
-
exports.RAFT_STATE_CONTINUATIONS_TOTAL = RAFT_STATE_CONTINUATIONS_TOTAL;
|
|
14117
|
-
exports.RAFT_STATE_SCHEMA = RAFT_STATE_SCHEMA;
|
|
14118
11825
|
exports.RaftCredentialError = RaftCredentialError;
|
|
14119
11826
|
exports.RaftSdkConfigurationError = RaftSdkConfigurationError;
|
|
14120
11827
|
exports.SeenFrontier = SeenFrontier;
|
|
@@ -14125,13 +11832,7 @@ exports.createRaftClient = createRaftClient;
|
|
|
14125
11832
|
exports.createRaftClientFromStore = createRaftClientFromStore;
|
|
14126
11833
|
exports.createRaftRoutes = createRaftRoutes;
|
|
14127
11834
|
exports.describeRaftRoute = describeRaftRoute;
|
|
14128
|
-
exports.hasAgentMessageIdentity = hasAgentMessageIdentity;
|
|
14129
|
-
exports.hashRaftSendContent = hashRaftSendContent;
|
|
14130
|
-
exports.inferAttachmentMimeType = inferAttachmentMimeType;
|
|
14131
11835
|
exports.joinRaftChannelByTarget = joinRaftChannelByTarget;
|
|
14132
11836
|
exports.listRaftRoutes = listRaftRoutes;
|
|
14133
11837
|
exports.parseRaftRegularChannelTarget = parseRaftRegularChannelTarget;
|
|
14134
|
-
exports.parseRaftState = parseRaftState;
|
|
14135
|
-
exports.projectRaftMessage = projectRaftMessage;
|
|
14136
|
-
exports.projectRaftMessages = projectRaftMessages;
|
|
14137
11838
|
exports.verifyInboxNotice = verifyInboxNotice;
|