@desplega.ai/agent-swarm 2.5.1 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
package/src/http/sessions.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
moveSessionChains,
|
|
27
27
|
type SessionKeyMove,
|
|
28
28
|
} from "../be/db/session-moves";
|
|
29
|
-
import { getSessionReadState, getSessionTree } from "../be/db/session-tree";
|
|
29
|
+
import { getSessionReadState, getSessionTree, triageSessionList } from "../be/db/session-tree";
|
|
30
30
|
import { getUserGrant } from "../be/rbac-roles";
|
|
31
31
|
import {
|
|
32
32
|
getSessionRoot,
|
|
@@ -157,6 +157,9 @@ const SessionTreeResponseSchema = z.object({
|
|
|
157
157
|
later: SessionSectionCountsSchema,
|
|
158
158
|
done: SessionSectionCountsSchema,
|
|
159
159
|
noGroup: SessionSectionCountsSchema,
|
|
160
|
+
sources: z
|
|
161
|
+
.array(z.object({ source: z.string(), total: z.number().int() }))
|
|
162
|
+
.describe("Each root source in this view with its session count, hidden sources included."),
|
|
160
163
|
});
|
|
161
164
|
|
|
162
165
|
const SessionApprovalSchema = z.object({
|
|
@@ -480,6 +483,43 @@ async function handleAgentTokens(
|
|
|
480
483
|
return true;
|
|
481
484
|
}
|
|
482
485
|
|
|
486
|
+
const SessionShowSchema = z
|
|
487
|
+
.enum(["mine", "all"])
|
|
488
|
+
.optional()
|
|
489
|
+
.describe(
|
|
490
|
+
"Sessions v2 view, for a user viewer with tree scope. mine (default): roots the user started from a human channel (not workflow, schedule, or system) plus sessions shared with the user. all: every tree session. Lists only: access is unchanged.",
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const SessionHideSourceSchema = z
|
|
494
|
+
.string()
|
|
495
|
+
// The same cap as the `hideSource` body field of `POST /api/session-triage`.
|
|
496
|
+
.refine((value) => value.split(",").length <= 32, "hideSource takes at most 32 sources.")
|
|
497
|
+
.optional()
|
|
498
|
+
.transform((value) =>
|
|
499
|
+
value
|
|
500
|
+
?.split(",")
|
|
501
|
+
.map((source) => source.trim())
|
|
502
|
+
.filter(Boolean),
|
|
503
|
+
)
|
|
504
|
+
.describe(
|
|
505
|
+
"Comma-separated root sources to leave out, e.g. `slack,chat`. Older sessions v2 clients send this; `showSource` replaces it.",
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const SessionShowSourceSchema = z
|
|
509
|
+
.string()
|
|
510
|
+
// The same cap as the `showSource` body field of `POST /api/session-triage`.
|
|
511
|
+
.refine((value) => value.split(",").length <= 32, "showSource takes at most 32 sources.")
|
|
512
|
+
.optional()
|
|
513
|
+
.transform((value) =>
|
|
514
|
+
value
|
|
515
|
+
?.split(",")
|
|
516
|
+
.map((source) => source.trim())
|
|
517
|
+
.filter(Boolean),
|
|
518
|
+
)
|
|
519
|
+
.describe(
|
|
520
|
+
"Comma-separated root sources to keep (the sessions v2 source filter), e.g. `ui,slack`. Present and empty keeps none; omit to keep every source.",
|
|
521
|
+
);
|
|
522
|
+
|
|
483
523
|
const listSessions = route({
|
|
484
524
|
method: "get",
|
|
485
525
|
path: "/api/sessions",
|
|
@@ -539,6 +579,12 @@ const listSessions = route({
|
|
|
539
579
|
.describe(
|
|
540
580
|
"mine: owned sessions. shared: sessions shared with me. tree: Sessions that GET /api/session-tree counts: mine plus shared with me, no ownerless roots. A bypass actor sees every root with tree scope.",
|
|
541
581
|
),
|
|
582
|
+
/** With `scope=tree`: `mine` (default) or `all`. Must match the tree's `show`. */
|
|
583
|
+
show: SessionShowSchema,
|
|
584
|
+
/** Root sources to leave out. Must match the tree's `hideSource`. */
|
|
585
|
+
hideSource: SessionHideSourceSchema,
|
|
586
|
+
/** Root sources to keep. Must match the tree's `showSource`. */
|
|
587
|
+
showSource: SessionShowSourceSchema,
|
|
542
588
|
}),
|
|
543
589
|
responses: {
|
|
544
590
|
200: {
|
|
@@ -561,6 +607,11 @@ const sessionTree = route({
|
|
|
561
607
|
pattern: ["api", "session-tree"],
|
|
562
608
|
summary: "Get session groups and triage counts",
|
|
563
609
|
tags: ["Sessions"],
|
|
610
|
+
query: z.object({
|
|
611
|
+
show: SessionShowSchema,
|
|
612
|
+
hideSource: SessionHideSourceSchema,
|
|
613
|
+
showSource: SessionShowSourceSchema,
|
|
614
|
+
}),
|
|
564
615
|
responses: {
|
|
565
616
|
200: { description: "Visible groups and section counts", schema: SessionTreeResponseSchema },
|
|
566
617
|
401: { description: "Unauthorized" },
|
|
@@ -568,6 +619,41 @@ const sessionTree = route({
|
|
|
568
619
|
auth: { apiKey: true },
|
|
569
620
|
});
|
|
570
621
|
|
|
622
|
+
const triageSessionListRoute = route({
|
|
623
|
+
method: "post",
|
|
624
|
+
path: "/api/session-triage",
|
|
625
|
+
pattern: ["api", "session-triage"],
|
|
626
|
+
summary: "Triage every session of one sidebar list",
|
|
627
|
+
description:
|
|
628
|
+
"The bulk bar's 'Select all N': Done, back to the Inbox, read, or unread for every session that one sessions v2 list shows the caller (the `GET /api/sessions?scope=tree` filters). Needs a picked user.",
|
|
629
|
+
tags: ["Sessions"],
|
|
630
|
+
body: z
|
|
631
|
+
.object({
|
|
632
|
+
action: z.enum(["done", "inbox", "read", "unread"]),
|
|
633
|
+
/** The list: one of Inbox, Later, Done, No group, or a group. */
|
|
634
|
+
triage: z.enum(["inbox", "later", "done"]).optional(),
|
|
635
|
+
noGroup: z.boolean().optional(),
|
|
636
|
+
groupKey: SessionKeyFilterSchema.optional(),
|
|
637
|
+
show: SessionShowSchema,
|
|
638
|
+
hideSource: z.array(z.string().min(1)).max(32).optional(),
|
|
639
|
+
/** Root sources to keep; an empty array keeps none. */
|
|
640
|
+
showSource: z.array(z.string().min(1)).max(32).optional(),
|
|
641
|
+
})
|
|
642
|
+
.refine((body) => [body.triage, body.noGroup, body.groupKey].filter(Boolean).length === 1, {
|
|
643
|
+
message: "Name exactly one list: triage, noGroup, or groupKey",
|
|
644
|
+
}),
|
|
645
|
+
responses: {
|
|
646
|
+
200: {
|
|
647
|
+
description: "The number of sessions written",
|
|
648
|
+
schema: z.object({ updated: z.number().int() }),
|
|
649
|
+
},
|
|
650
|
+
400: { description: "Validation error, or no picked user" },
|
|
651
|
+
401: { description: "Unauthorized" },
|
|
652
|
+
},
|
|
653
|
+
auth: { apiKey: true },
|
|
654
|
+
rbac: { ungated: "per-user inbox UI state, same posture as PATCH /api/inbox-state" },
|
|
655
|
+
});
|
|
656
|
+
|
|
571
657
|
const SESSION_MOVE_LIMIT = 200;
|
|
572
658
|
const SESSION_MOVE_OWNER_ONLY = "Only the session owner can move this group";
|
|
573
659
|
const SESSION_MOVE_UNAVAILABLE = "Session not found or not accessible";
|
|
@@ -877,6 +963,8 @@ export async function handleSessions(
|
|
|
877
963
|
}
|
|
878
964
|
const filterOpts = {
|
|
879
965
|
source: sources,
|
|
966
|
+
hideSource: parsed.query.hideSource,
|
|
967
|
+
showSource: parsed.query.showSource,
|
|
880
968
|
q: parsed.query.q,
|
|
881
969
|
requestedByUserId: parsed.query.requestedByUserId,
|
|
882
970
|
contextKeyPrefix: parsed.query.contextKeyPrefix,
|
|
@@ -888,6 +976,7 @@ export async function handleSessions(
|
|
|
888
976
|
withUserId: parsed.query.withUserId,
|
|
889
977
|
now: new Date().toISOString(),
|
|
890
978
|
tree: parsed.query.scope === "tree",
|
|
979
|
+
mine: parsed.query.scope === "tree" && parsed.query.show !== "all",
|
|
891
980
|
...(actor.kind === "user"
|
|
892
981
|
? {
|
|
893
982
|
viewer: {
|
|
@@ -922,7 +1011,34 @@ export async function handleSessions(
|
|
|
922
1011
|
if (sessionTree.match(req.method, pathSegments)) {
|
|
923
1012
|
const parsed = await sessionTree.parse(req, res, pathSegments, queryParams);
|
|
924
1013
|
if (!parsed) return true;
|
|
925
|
-
|
|
1014
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
1015
|
+
sessionTree.respond(
|
|
1016
|
+
res,
|
|
1017
|
+
200,
|
|
1018
|
+
await getSessionTree(actor, undefined, {
|
|
1019
|
+
mine: parsed.query.show !== "all",
|
|
1020
|
+
hideSource: parsed.query.hideSource,
|
|
1021
|
+
showSource: parsed.query.showSource,
|
|
1022
|
+
}),
|
|
1023
|
+
);
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (triageSessionListRoute.match(req.method, pathSegments)) {
|
|
1028
|
+
const parsed = await triageSessionListRoute.parse(req, res, pathSegments, queryParams);
|
|
1029
|
+
if (!parsed) return true;
|
|
1030
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
1031
|
+
if (actor.kind !== "user" || !actor.userId) {
|
|
1032
|
+
jsonError(res, "Pick a user to triage sessions", 400);
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
const { action, show, ...list } = parsed.body;
|
|
1036
|
+
const updated = await triageSessionList(
|
|
1037
|
+
actor.userId,
|
|
1038
|
+
{ ...list, mine: show !== "all" },
|
|
1039
|
+
action,
|
|
1040
|
+
);
|
|
1041
|
+
triageSessionListRoute.respond(res, 200, { updated });
|
|
926
1042
|
return true;
|
|
927
1043
|
}
|
|
928
1044
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RBAC role assignment for people.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/rbac/roles
|
|
5
|
+
* GET /api/users/:id/roles
|
|
6
|
+
* PUT /api/users/:id/roles
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { getUserById } from "../be/db";
|
|
12
|
+
import {
|
|
13
|
+
getUserGrant,
|
|
14
|
+
InvalidRoleSetError,
|
|
15
|
+
LastAdminError,
|
|
16
|
+
listRbacRoles,
|
|
17
|
+
listUserRoles,
|
|
18
|
+
PRIMARY_ROLE_NAMES,
|
|
19
|
+
setUserRoles,
|
|
20
|
+
} from "../be/rbac-roles";
|
|
21
|
+
import { recordIdentityEvent } from "../be/users";
|
|
22
|
+
import { can, isRbacEnabled } from "../rbac";
|
|
23
|
+
import { RbacRoleSchema, UserRbacRolesSchema } from "../types";
|
|
24
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
25
|
+
import { getOperatorActor } from "./operator-actor";
|
|
26
|
+
import { agentFirstPrincipal, rejectGuest } from "./request-principal";
|
|
27
|
+
import { route } from "./route-def";
|
|
28
|
+
import { jsonError } from "./utils";
|
|
29
|
+
|
|
30
|
+
const listRolesRoute = route({
|
|
31
|
+
method: "get",
|
|
32
|
+
path: "/api/rbac/roles",
|
|
33
|
+
pattern: ["api", "rbac", "roles"],
|
|
34
|
+
operationId: "rbac_listRoles",
|
|
35
|
+
summary: "List RBAC roles with their verbs",
|
|
36
|
+
tags: ["Users"],
|
|
37
|
+
responses: {
|
|
38
|
+
200: { description: "Role catalog", schema: z.object({ roles: z.array(RbacRoleSchema) }) },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const getUserRolesRoute = route({
|
|
43
|
+
method: "get",
|
|
44
|
+
path: "/api/users/{id}/roles",
|
|
45
|
+
pattern: ["api", "users", null, "roles"],
|
|
46
|
+
operationId: "users_getRbacRoles",
|
|
47
|
+
summary: "Get a user's RBAC roles",
|
|
48
|
+
tags: ["Users"],
|
|
49
|
+
params: z.object({ id: z.string().min(1) }),
|
|
50
|
+
responses: {
|
|
51
|
+
200: { description: "The user's roles", schema: UserRbacRolesSchema },
|
|
52
|
+
404: { description: "User not found" },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const setUserRolesRoute = route({
|
|
57
|
+
method: "put",
|
|
58
|
+
path: "/api/users/{id}/roles",
|
|
59
|
+
pattern: ["api", "users", null, "roles"],
|
|
60
|
+
operationId: "users_setRbacRoles",
|
|
61
|
+
summary: "Set a user's RBAC roles",
|
|
62
|
+
description: `Replaces the user's roles. The set holds exactly one of ${PRIMARY_ROLE_NAMES.join(", ")} plus any extra roles. Demoting a user below admin removes the ability to answer approval requests, even when the user is listed as an approver. Fails with 409 when it would leave no active admin.`,
|
|
63
|
+
tags: ["Users"],
|
|
64
|
+
params: z.object({ id: z.string().min(1) }),
|
|
65
|
+
body: z.object({ roles: z.array(z.string().min(1)).min(1) }).strict(),
|
|
66
|
+
responses: {
|
|
67
|
+
200: { description: "The user's roles after the change", schema: UserRbacRolesSchema },
|
|
68
|
+
400: { description: "Invalid role set" },
|
|
69
|
+
403: { description: "Not allowed to change roles" },
|
|
70
|
+
404: { description: "User not found" },
|
|
71
|
+
409: { description: "Would leave no active admin" },
|
|
72
|
+
},
|
|
73
|
+
rbac: { permission: "user.role.manage" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function composeRoles(userId: string) {
|
|
77
|
+
const [catalog, held] = await Promise.all([listRbacRoles(), listUserRoles(userId)]);
|
|
78
|
+
const names = new Set(held.map((role) => role.name));
|
|
79
|
+
return { userId, roles: catalog.filter((role) => names.has(role.name)) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Operator or user only; agents never change human roles, even with the shared key. */
|
|
83
|
+
async function ensureRoleManager(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
84
|
+
if (rejectGuest(req, res)) return false;
|
|
85
|
+
const auth = getRequestAuth(req);
|
|
86
|
+
if (auth?.kind !== "operator" && auth?.kind !== "user") {
|
|
87
|
+
jsonError(res, "Role changes require operator or user credentials", 403);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const header = req.headers["x-agent-id"];
|
|
91
|
+
const decision = can({
|
|
92
|
+
principal: await agentFirstPrincipal(req, Array.isArray(header) ? header[0] : header),
|
|
93
|
+
verb: "user.role.manage",
|
|
94
|
+
resource: { kind: "none" },
|
|
95
|
+
source: "http",
|
|
96
|
+
});
|
|
97
|
+
if (!decision.allow) {
|
|
98
|
+
jsonError(res, decision.reason, 403);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// A custom role holding user.role.manage must not be able to grant admin.
|
|
102
|
+
if (auth.kind === "user" && isRbacEnabled() && !(await getUserGrant(auth.userId)).grantsAll) {
|
|
103
|
+
jsonError(res, "Role changes require a grant-all role", 403);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function handleUserRoles(
|
|
110
|
+
req: IncomingMessage,
|
|
111
|
+
res: ServerResponse,
|
|
112
|
+
pathSegments: string[],
|
|
113
|
+
queryParams: URLSearchParams,
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
if (listRolesRoute.match(req.method, pathSegments)) {
|
|
116
|
+
if (rejectGuest(req, res)) return true;
|
|
117
|
+
listRolesRoute.respond(res, 200, { roles: await listRbacRoles() });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (getUserRolesRoute.match(req.method, pathSegments)) {
|
|
122
|
+
if (rejectGuest(req, res)) return true;
|
|
123
|
+
const parsed = await getUserRolesRoute.parse(req, res, pathSegments, queryParams);
|
|
124
|
+
if (!parsed) return true;
|
|
125
|
+
if (!(await getUserById(parsed.params.id))) {
|
|
126
|
+
jsonError(res, "User not found", 404);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
getUserRolesRoute.respond(res, 200, await composeRoles(parsed.params.id));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (setUserRolesRoute.match(req.method, pathSegments)) {
|
|
134
|
+
const parsed = await setUserRolesRoute.parse(req, res, pathSegments, queryParams);
|
|
135
|
+
if (!parsed) return true;
|
|
136
|
+
if (!(await ensureRoleManager(req, res))) return true;
|
|
137
|
+
const actor = getOperatorActor(req, res);
|
|
138
|
+
if (!actor) return true;
|
|
139
|
+
const userId = parsed.params.id;
|
|
140
|
+
if (!(await getUserById(userId))) {
|
|
141
|
+
jsonError(res, "User not found", 404);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const { before, after } = await setUserRoles(
|
|
146
|
+
userId,
|
|
147
|
+
parsed.body.roles,
|
|
148
|
+
actor.kind === "user" ? actor.id : null,
|
|
149
|
+
);
|
|
150
|
+
await recordIdentityEvent(
|
|
151
|
+
userId,
|
|
152
|
+
"profile_changed",
|
|
153
|
+
actor,
|
|
154
|
+
{ rbacRoles: before },
|
|
155
|
+
{ rbacRoles: after },
|
|
156
|
+
);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof LastAdminError) {
|
|
159
|
+
jsonError(res, "last_admin", 409);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (err instanceof InvalidRoleSetError) {
|
|
163
|
+
jsonError(res, err.message, 400);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
setUserRolesRoute.respond(res, 200, await composeRoles(userId));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
}
|
package/src/http/users.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
updateUser,
|
|
46
46
|
upsertBudget,
|
|
47
47
|
} from "../be/db";
|
|
48
|
+
import { assertNotLastAdmin, LastAdminError } from "../be/rbac-roles";
|
|
48
49
|
import {
|
|
49
50
|
getUserIdentities,
|
|
50
51
|
type IdentityEvent,
|
|
@@ -484,6 +485,7 @@ const mergeUsersRoute = route({
|
|
|
484
485
|
400: { description: "Validation error (e.g. target == source)" },
|
|
485
486
|
401: { description: "Unauthorized" },
|
|
486
487
|
404: { description: "Target or source user not found" },
|
|
488
|
+
409: { description: "Source is the last active admin (last_admin)" },
|
|
487
489
|
},
|
|
488
490
|
auth: { apiKey: true },
|
|
489
491
|
});
|
|
@@ -1071,6 +1073,9 @@ export async function handleUsers(
|
|
|
1071
1073
|
}
|
|
1072
1074
|
}
|
|
1073
1075
|
|
|
1076
|
+
// The source's roles go with it; never leave the swarm without an admin.
|
|
1077
|
+
await assertNotLastAdmin(sourceId);
|
|
1078
|
+
|
|
1074
1079
|
// Delete source — CASCADE cleans up any leftover external_ids row that
|
|
1075
1080
|
// we may have missed. Retained user references move to the target.
|
|
1076
1081
|
await deleteUser(sourceId, targetId);
|
|
@@ -1095,6 +1100,10 @@ export async function handleUsers(
|
|
|
1095
1100
|
});
|
|
1096
1101
|
mergeUsersRoute.respond(res, 200, { user: mergedUser });
|
|
1097
1102
|
} catch (err) {
|
|
1103
|
+
if (err instanceof LastAdminError) {
|
|
1104
|
+
jsonError(res, "last_admin", 409);
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1098
1107
|
jsonError(res, err instanceof Error ? err.message : "Failed to merge users", 500);
|
|
1099
1108
|
}
|
|
1100
1109
|
return true;
|
package/src/http/x.ts
CHANGED
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
restoreScratchScriptLastUsedIfUnchanged,
|
|
16
16
|
touchScratchScriptLastUsed,
|
|
17
17
|
} from "../be/scripts/db";
|
|
18
|
+
import { runScriptWithRunCredential } from "../be/scripts/run-credential";
|
|
18
19
|
import {
|
|
19
20
|
MAX_SCRIPT_WALL_CLOCK_MS,
|
|
20
21
|
MIN_SCRIPT_WALL_CLOCK_MS,
|
|
21
22
|
} from "../scripts-runtime/executors/types";
|
|
22
23
|
import type { RunScriptOutput } from "../scripts-runtime/loader";
|
|
23
|
-
import { runScript } from "../scripts-runtime/loader";
|
|
24
24
|
import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
|
|
25
25
|
import { validateJsonSchema } from "../workflows/json-schema-validator";
|
|
26
26
|
import { route } from "./route-def";
|
|
@@ -242,7 +242,7 @@ export async function handleX(
|
|
|
242
242
|
const credentials = await buildScriptCredentialBindingsWithFailures({
|
|
243
243
|
agentId: endpoint.agentId,
|
|
244
244
|
});
|
|
245
|
-
const output = await
|
|
245
|
+
const output = await runScriptWithRunCredential({
|
|
246
246
|
source: script.source,
|
|
247
247
|
args: args ?? null,
|
|
248
248
|
fsMode: "none",
|
|
@@ -63,7 +63,19 @@ function helpText(): string {
|
|
|
63
63
|
return ["Commands:", ...CHAT_COMMANDS.map((c) => `/${c.command}: ${c.description}`)].join("\n");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Telegram sends `/start` when a user opens a bot DM. It stays out of
|
|
68
|
+
* `CHAT_COMMANDS`, so the `/` menu does not list it.
|
|
69
|
+
*/
|
|
70
|
+
function welcomeText(): string {
|
|
71
|
+
return [
|
|
72
|
+
"Hi! Send a message here (in a group, mention me) and the swarm picks it up. Each answer arrives as a reply to your message.",
|
|
73
|
+
"",
|
|
74
|
+
helpText(),
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function shortId(task: Pick<AgentTask, "id">): string {
|
|
67
79
|
return task.id.slice(0, 8);
|
|
68
80
|
}
|
|
69
81
|
|
|
@@ -115,6 +127,7 @@ export async function runChatCommand(
|
|
|
115
127
|
): Promise<string> {
|
|
116
128
|
const name = command.replace(/^\//, "").toLowerCase();
|
|
117
129
|
if (name === "help") return helpText();
|
|
130
|
+
if (name === "start") return welcomeText();
|
|
118
131
|
if (!CHAT_COMMANDS.some((c) => c.command === name)) {
|
|
119
132
|
return `Unknown command /${name}.\n${helpText()}`;
|
|
120
133
|
}
|
|
@@ -32,6 +32,12 @@ import { getChatThreadResetAt } from "./commands";
|
|
|
32
32
|
import { type ChatAddressedReason, formatChatHeaderFacts, readChatMessageFacts } from "./facts";
|
|
33
33
|
import { errorMessage } from "./logger";
|
|
34
34
|
import { emitChatMessageSent, postChatMessage } from "./outbound";
|
|
35
|
+
import {
|
|
36
|
+
chatAdapterReacts,
|
|
37
|
+
clearChatMessageReactions,
|
|
38
|
+
setChatMessageReaction,
|
|
39
|
+
wantsChatTextAck,
|
|
40
|
+
} from "./reactions";
|
|
35
41
|
import {
|
|
36
42
|
type ChatAgentMatch,
|
|
37
43
|
type ChatRouteTarget,
|
|
@@ -154,9 +160,14 @@ export async function handleChatInbound(
|
|
|
154
160
|
console.log(
|
|
155
161
|
`[chat-sdk] ${adapter} message steered task ${steering.task.id} (${steering.result.outcome})`,
|
|
156
162
|
);
|
|
163
|
+
await setChatMessageReaction(adapter, thread.id, message.id, "steered");
|
|
164
|
+
if (!(await wantsChatTextAck(adapter, thread.channelId))) return;
|
|
157
165
|
try {
|
|
158
166
|
const ack = formatChatSteeringAck(steering.result);
|
|
159
|
-
|
|
167
|
+
// Telegram threads the ack under the steering message; other adapters post it.
|
|
168
|
+
const sent = thread.adapter.reply
|
|
169
|
+
? await thread.reply(message, { markdown: ack })
|
|
170
|
+
: await thread.post({ markdown: ack });
|
|
160
171
|
emitChatMessageSent({
|
|
161
172
|
adapter,
|
|
162
173
|
channelId: thread.channelId,
|
|
@@ -177,6 +188,10 @@ export async function handleChatInbound(
|
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
if (text || attachments.length > 0) {
|
|
191
|
+
// An unaddressed subscribed-thread message may be dropped by the router at
|
|
192
|
+
// flush time; it gets typing and a reaction once a task exists (see
|
|
193
|
+
// flushChatThread). A mention, reply to the bot, or DM is acknowledged now.
|
|
194
|
+
const acknowledge = kind !== "subscribed" || isDirectMessage || mentioned || facts.replyToBot;
|
|
180
195
|
bufferChatMessage(thread.id, {
|
|
181
196
|
messageId: message.id,
|
|
182
197
|
channelId: thread.channelId,
|
|
@@ -186,12 +201,14 @@ export async function handleChatInbound(
|
|
|
186
201
|
botMentioned: mentioned || facts.replyToBot,
|
|
187
202
|
isDirectMessage,
|
|
188
203
|
addressed,
|
|
204
|
+
reacted: acknowledge && chatAdapterReacts(adapter),
|
|
189
205
|
facts,
|
|
190
206
|
attachments,
|
|
191
207
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
if (acknowledge) {
|
|
209
|
+
startTypingKeepalive(adapter, thread.id);
|
|
210
|
+
await setChatMessageReaction(adapter, thread.id, message.id, immediate ? "now" : "accepted");
|
|
211
|
+
}
|
|
195
212
|
}
|
|
196
213
|
if (immediate) await instantFlushChat(thread.id);
|
|
197
214
|
}
|
|
@@ -377,6 +394,9 @@ export async function flushChatThread(
|
|
|
377
394
|
// One task carries every sender's text, so a batch from several people
|
|
378
395
|
// gets no requester: one sender's identity must not vouch for another's words.
|
|
379
396
|
const otherSenders = new Set(items.map((item) => item.userId)).size - 1;
|
|
397
|
+
// The task answers the newest message; it alone carries the reaction lifecycle.
|
|
398
|
+
const answered = items.at(-1) ?? first;
|
|
399
|
+
const reacted = items.filter((item) => item.reacted).map((item) => item.messageId);
|
|
380
400
|
|
|
381
401
|
// After `/reset` the thread's earlier tasks are not context: the new task gets
|
|
382
402
|
// no parent and no thread history older than the reset.
|
|
@@ -418,6 +438,7 @@ export async function flushChatThread(
|
|
|
418
438
|
scrubSecrets(routeResult.reason),
|
|
419
439
|
);
|
|
420
440
|
stopTypingKeepalive(threadId);
|
|
441
|
+
await clearChatMessageReactions(adapter, threadId, reacted);
|
|
421
442
|
return;
|
|
422
443
|
}
|
|
423
444
|
|
|
@@ -433,12 +454,14 @@ export async function flushChatThread(
|
|
|
433
454
|
if (!target) {
|
|
434
455
|
console.log(`[chat-sdk] ${threadId}: message not addressed to the swarm, ignored`);
|
|
435
456
|
stopTypingKeepalive(threadId);
|
|
457
|
+
await clearChatMessageReactions(adapter, threadId, reacted);
|
|
436
458
|
return;
|
|
437
459
|
}
|
|
438
460
|
|
|
439
461
|
if (target.kind === "workflow") {
|
|
440
|
-
// A workflow run has no relay that would stop the indicator.
|
|
462
|
+
// A workflow run has no relay that would stop the indicator or end the reaction.
|
|
441
463
|
stopTypingKeepalive(threadId);
|
|
464
|
+
await clearChatMessageReactions(adapter, threadId, reacted);
|
|
442
465
|
try {
|
|
443
466
|
// The adapter verified the platform signature, and the workflow id comes
|
|
444
467
|
// from an operator mapping or an extension, never from the message.
|
|
@@ -533,6 +556,11 @@ export async function flushChatThread(
|
|
|
533
556
|
// A human message in the thread: never answer it in the previous task's
|
|
534
557
|
// structured-output contract.
|
|
535
558
|
inheritParentOutputSchema: false,
|
|
559
|
+
// The relay answers as a reply to the newest buffered message. Always set
|
|
560
|
+
// explicitly: `createTask` copies the parent's anchor into a child that
|
|
561
|
+
// has none, and that would point at an older ask.
|
|
562
|
+
mentionMessageId: answered.messageId,
|
|
563
|
+
mentionChannelId: first.channelId,
|
|
536
564
|
};
|
|
537
565
|
try {
|
|
538
566
|
const { task, unattached } = await createChatTaskWithFiles(description, options, inbound);
|
|
@@ -553,10 +581,18 @@ export async function flushChatThread(
|
|
|
553
581
|
if (created > 0) {
|
|
554
582
|
await rememberChatThreadRequester(threadId, requester.userId);
|
|
555
583
|
await subscribe(thread);
|
|
556
|
-
//
|
|
584
|
+
// Unaddressed subscribed-thread messages start typing only here, once a task exists.
|
|
557
585
|
startTypingKeepalive(adapter, threadId);
|
|
586
|
+
await clearChatMessageReactions(
|
|
587
|
+
adapter,
|
|
588
|
+
threadId,
|
|
589
|
+
reacted.filter((id) => id !== answered.messageId),
|
|
590
|
+
);
|
|
591
|
+
if (!answered.reacted)
|
|
592
|
+
await setChatMessageReaction(adapter, threadId, answered.messageId, "accepted");
|
|
558
593
|
} else {
|
|
559
594
|
stopTypingKeepalive(threadId);
|
|
595
|
+
await clearChatMessageReactions(adapter, threadId, reacted);
|
|
560
596
|
}
|
|
561
597
|
}
|
|
562
598
|
|
|
@@ -76,12 +76,14 @@ export class ChatFileTooLargeError extends Error {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
type SentChunk = { id: string; threadId?: string };
|
|
80
|
+
|
|
79
81
|
interface PostTarget {
|
|
80
82
|
post(message: {
|
|
81
83
|
markdown: string;
|
|
82
84
|
files?: FileUpload[];
|
|
83
85
|
attachments?: Attachment[];
|
|
84
|
-
}): Promise<
|
|
86
|
+
}): Promise<SentChunk>;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** A fence opener or closer: up to 3 spaces, then 3+ backticks or tildes. */
|
|
@@ -120,16 +122,22 @@ export function escapeTelegramHashLines(markdown: string): string {
|
|
|
120
122
|
.join("\n");
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** `replyFirst` sends the first chunk as a native reply; later chunks are plain posts. */
|
|
123
126
|
async function postChunks(
|
|
124
127
|
adapter: string,
|
|
125
128
|
target: PostTarget,
|
|
126
|
-
|
|
129
|
+
parts: readonly string[],
|
|
130
|
+
replyFirst?: (message: { markdown: string }) => Promise<SentChunk>,
|
|
127
131
|
): Promise<{ messageIds: string[]; threadId?: string }> {
|
|
128
132
|
const messageIds: string[] = [];
|
|
129
133
|
let threadId: string | undefined;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
134
|
+
const limit = chatMessageLimit(adapter);
|
|
135
|
+
for (const chunk of parts.flatMap((part) => splitChatMessage(part, limit))) {
|
|
136
|
+
const message = { markdown: adapter === "telegram" ? escapeTelegramHashLines(chunk) : chunk };
|
|
137
|
+
const sent =
|
|
138
|
+
replyFirst && messageIds.length === 0
|
|
139
|
+
? await replyFirst(message)
|
|
140
|
+
: await target.post(message);
|
|
133
141
|
messageIds.push(sent.id);
|
|
134
142
|
threadId ??= sent.threadId;
|
|
135
143
|
}
|
|
@@ -323,28 +331,45 @@ export function formatUnsentFilesNote(unsent: UnsentChatFile[]): string {
|
|
|
323
331
|
* Delivery is at-least-once: a chunk failure re-posts earlier chunks on retry,
|
|
324
332
|
* and a crash between the send and `markChatRelayDelivered` re-sends the
|
|
325
333
|
* message.
|
|
334
|
+
*
|
|
335
|
+
* An array `text` is posted as one message per element, each still split at
|
|
336
|
+
* the limit; the caller uses it when its own markup must not be cut in two.
|
|
337
|
+
*
|
|
338
|
+
* `replyToMessageId` anchors the first chunk to that message on a thread
|
|
339
|
+
* target. Telegram sends it with `allow_sending_without_reply`, so a deleted
|
|
340
|
+
* target still gets the message, unthreaded.
|
|
326
341
|
*/
|
|
327
342
|
export async function postChatMessage(args: {
|
|
328
343
|
adapter: string;
|
|
329
344
|
threadId?: string;
|
|
330
345
|
channelId?: string;
|
|
331
|
-
text: string;
|
|
346
|
+
text: string | readonly string[];
|
|
332
347
|
taskId?: string;
|
|
333
348
|
files?: OutboundChatFile[];
|
|
349
|
+
replyToMessageId?: string;
|
|
334
350
|
}): Promise<ChatPostResult> {
|
|
335
351
|
// Ids come from tools and stored context keys. Check the Teams origin
|
|
336
352
|
// before the adapter sends its token.
|
|
337
353
|
assertTrustedChatTarget(args.threadId ?? args.channelId ?? "");
|
|
338
354
|
const chat = getChatInstance(args.adapter);
|
|
339
355
|
if (!chat) throw new ChatAdapterUnavailableError(args.adapter);
|
|
340
|
-
const
|
|
356
|
+
const parts = (typeof args.text === "string" ? [args.text] : args.text).map(scrubSecrets);
|
|
357
|
+
const text = parts.join("\n\n");
|
|
341
358
|
|
|
342
359
|
let result: ChatPostResult;
|
|
343
360
|
let target: PostTarget;
|
|
344
361
|
if (args.threadId) {
|
|
345
362
|
const thread = chat.thread(args.threadId);
|
|
346
363
|
target = thread;
|
|
347
|
-
|
|
364
|
+
// Only adapters with a native reply (Telegram in this SDK version) anchor;
|
|
365
|
+
// the others post unanchored, as before.
|
|
366
|
+
const replyTo = args.replyToMessageId;
|
|
367
|
+
const sent = await postChunks(
|
|
368
|
+
args.adapter,
|
|
369
|
+
thread,
|
|
370
|
+
parts,
|
|
371
|
+
replyTo && thread.adapter.reply ? (message) => thread.reply(replyTo, message) : undefined,
|
|
372
|
+
);
|
|
348
373
|
result = {
|
|
349
374
|
adapter: args.adapter,
|
|
350
375
|
threadId: thread.id,
|
|
@@ -355,7 +380,7 @@ export async function postChatMessage(args: {
|
|
|
355
380
|
} else if (args.channelId) {
|
|
356
381
|
const channel = chat.channel(args.channelId);
|
|
357
382
|
target = channel;
|
|
358
|
-
const sent = await postChunks(args.adapter, channel,
|
|
383
|
+
const sent = await postChunks(args.adapter, channel, parts);
|
|
359
384
|
result = {
|
|
360
385
|
adapter: args.adapter,
|
|
361
386
|
threadId: sent.threadId ?? channel.id,
|
|
@@ -373,7 +398,7 @@ export async function postChatMessage(args: {
|
|
|
373
398
|
result.unsentFiles = sent.unsent;
|
|
374
399
|
if (sent.unsent.length > 0) {
|
|
375
400
|
const note = formatUnsentFilesNote(sent.unsent);
|
|
376
|
-
const noted = await postChunks(args.adapter, target, scrubSecrets(note));
|
|
401
|
+
const noted = await postChunks(args.adapter, target, [scrubSecrets(note)]);
|
|
377
402
|
result.messageIds.push(...noted.messageIds);
|
|
378
403
|
}
|
|
379
404
|
}
|