@cursor/july 0.1.114 → 0.2.1
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/channels/slack/channel-watch.d.ts +19 -3
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +48 -9
- package/dist/channels/slack/inbound.d.ts +7 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +23 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +87 -8
- package/dist/channels/slack/types.d.ts +11 -12
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/assets/{app.BqkJwOZ-.js → app.D23Y-7Tp.js} +4 -4
- package/dist/docs/assets/chunks/@localSearchIndexroot.CzCCM7N8.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BJAi2KiV.js → VPLocalSearchBox.CWBeTFRZ.js} +1 -1
- package/dist/docs/assets/chunks/{arc.BZpXTgvV.js → arc.DSF2O3pm.js} +1 -1
- package/dist/docs/assets/chunks/{architectureDiagram-Q4EWVU46.WYI-7F-Y.js → architectureDiagram-Q4EWVU46.J52Wzbkg.js} +1 -1
- package/dist/docs/assets/chunks/{baseUniq.CZaUPpg0.js → baseUniq.CQS3LPCt.js} +1 -1
- package/dist/docs/assets/chunks/{blockDiagram-DXYQGD6D.D6UES2pD.js → blockDiagram-DXYQGD6D.Dw339Gr5.js} +1 -1
- package/dist/docs/assets/chunks/{c4Diagram-AHTNJAMY.cwebIe4i.js → c4Diagram-AHTNJAMY.BUdtOaRZ.js} +1 -1
- package/dist/docs/assets/chunks/channel.Bfu4df88.js +1 -0
- package/dist/docs/assets/chunks/{chunk-4BX2VUAB.fVyFnjxg.js → chunk-4BX2VUAB.CuOrkEqk.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-4TB4RGXK.BanufG1c.js → chunk-4TB4RGXK.BJNBcY7U.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-55IACEB6.VaSMz5-2.js → chunk-55IACEB6.VJK5LAm_.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-EDXVE4YY.CN2diZOM.js → chunk-EDXVE4YY.BYYLihvj.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-FMBD7UC4.g4ivypu3.js → chunk-FMBD7UC4.CmoW8BXP.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-OYMX7WX6.GZXKn9JJ.js → chunk-OYMX7WX6.DTGY4C-M.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-QZHKN3VN.itXxJZCd.js → chunk-QZHKN3VN.Cg5n67vl.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-YZCP3GAM.-rw2GfvX.js → chunk-YZCP3GAM.C3GR_ia5.js} +1 -1
- package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.DdfgtaWs.js +1 -0
- package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.DdfgtaWs.js +1 -0
- package/dist/docs/assets/chunks/clone.rkmfti6d.js +1 -0
- package/dist/docs/assets/chunks/{cose-bilkent-S5V4N54A.CmaI5br0.js → cose-bilkent-S5V4N54A.BTRG8N3b.js} +1 -1
- package/dist/docs/assets/chunks/{dagre-KV5264BT.4wY9S4Kt.js → dagre-KV5264BT.Bob_bp_p.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-5BDNPKRD.Pc3c0u9W.js → diagram-5BDNPKRD.ggPcs9uO.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-G4DWMVQ6.CYrWz-nj.js → diagram-G4DWMVQ6.BP0qyJkp.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-MMDJMWI5.Bgj5hukb.js → diagram-MMDJMWI5.B0X24UKr.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-TYMM5635.DGMEXalS.js → diagram-TYMM5635.B4rXHFVt.js} +1 -1
- package/dist/docs/assets/chunks/{erDiagram-SMLLAGMA.GepTV9Im.js → erDiagram-SMLLAGMA._55Rt9oX.js} +1 -1
- package/dist/docs/assets/chunks/{flowDiagram-DWJPFMVM.DVKywg3j.js → flowDiagram-DWJPFMVM.DGP4XvR5.js} +1 -1
- package/dist/docs/assets/chunks/{ganttDiagram-T4ZO3ILL.C7qt9Mlo.js → ganttDiagram-T4ZO3ILL.BtXtkL4E.js} +1 -1
- package/dist/docs/assets/chunks/{gitGraphDiagram-UUTBAWPF.U30_r82P.js → gitGraphDiagram-UUTBAWPF.B9cPWblK.js} +1 -1
- package/dist/docs/assets/chunks/{graph.CyyMyAWv.js → graph.D8HzNexS.js} +1 -1
- package/dist/docs/assets/chunks/{infoDiagram-42DDH7IO.Dn9ACW3y.js → infoDiagram-42DDH7IO.Bw7CQUpi.js} +1 -1
- package/dist/docs/assets/chunks/{ishikawaDiagram-UXIWVN3A.DlIdIGOA.js → ishikawaDiagram-UXIWVN3A.MwkzF6nQ.js} +1 -1
- package/dist/docs/assets/chunks/{journeyDiagram-VCZTEJTY.DZj4vy4E.js → journeyDiagram-VCZTEJTY.DIGFF-3C.js} +1 -1
- package/dist/docs/assets/chunks/{kanban-definition-6JOO6SKY.Dl63eMUV.js → kanban-definition-6JOO6SKY.DhYef2BN.js} +1 -1
- package/dist/docs/assets/chunks/{layout.BLHZLWPH.js → layout.C0XUxuPi.js} +1 -1
- package/dist/docs/assets/chunks/{linear.aXKGKaNw.js → linear.BwNPpZex.js} +1 -1
- package/dist/docs/assets/chunks/{min.zWnFcpcc.js → min.CwAQdL7z.js} +1 -1
- package/dist/docs/assets/chunks/{mindmap-definition-QFDTVHPH.Qs4MQBea.js → mindmap-definition-QFDTVHPH.pWsSVLsP.js} +1 -1
- package/dist/docs/assets/chunks/{pieDiagram-DEJITSTG.BmPHgsk7.js → pieDiagram-DEJITSTG.BDJ3FbBy.js} +1 -1
- package/dist/docs/assets/chunks/{quadrantDiagram-34T5L4WZ.D5MQ3gwA.js → quadrantDiagram-34T5L4WZ.Co80izyB.js} +1 -1
- package/dist/docs/assets/chunks/{requirementDiagram-MS252O5E.CkdUFrO7.js → requirementDiagram-MS252O5E.JveKw4yx.js} +1 -1
- package/dist/docs/assets/chunks/{sankeyDiagram-XADWPNL6.KZrljrAV.js → sankeyDiagram-XADWPNL6.B0A7adPi.js} +1 -1
- package/dist/docs/assets/chunks/{sequenceDiagram-FGHM5R23.XMoEW-Lx.js → sequenceDiagram-FGHM5R23.d6JZ5Hre.js} +1 -1
- package/dist/docs/assets/chunks/{stateDiagram-FHFEXIEX.BmTzePLj.js → stateDiagram-FHFEXIEX.DWnL0NQl.js} +1 -1
- package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.ZEetPk0E.js +1 -0
- package/dist/docs/assets/chunks/{theme.BfQzpxsg.js → theme.MJTLx0hh.js} +2 -2
- package/dist/docs/assets/chunks/{timeline-definition-GMOUNBTQ.Dug0oamp.js → timeline-definition-GMOUNBTQ.CFS7Ai4c.js} +1 -1
- package/dist/docs/assets/chunks/{vennDiagram-DHZGUBPP.BOTHrEFu.js → vennDiagram-DHZGUBPP.CwSlnjCf.js} +1 -1
- package/dist/docs/assets/chunks/wardley-RL74JXVD.3gurI8YA.js +162 -0
- package/dist/docs/assets/chunks/{wardleyDiagram-NUSXRM2D.CoXKdfi6.js → wardleyDiagram-NUSXRM2D.B_8mvtjh.js} +1 -1
- package/dist/docs/assets/chunks/{xychartDiagram-5P7HB3ND.DXoSCjAW.js → xychartDiagram-5P7HB3ND.DtjU5H85.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.Bo96y42E.js → guides_slack.md.Bjw2r2gL.js} +5 -5
- package/dist/docs/assets/{guides_slack.md.Bo96y42E.lean.js → guides_slack.md.Bjw2r2gL.lean.js} +1 -1
- package/dist/docs/assets/reference_cli.md.BvnQM8wd.js +97 -0
- package/dist/docs/assets/reference_cli.md.BvnQM8wd.lean.js +1 -0
- package/dist/docs/building-with-agents.html +35 -35
- package/dist/docs/deployment.html +35 -35
- package/dist/docs/evals.html +35 -35
- package/dist/docs/guides/agent-to-agent.html +35 -35
- package/dist/docs/guides/bitbucket.html +35 -35
- package/dist/docs/guides/cloud-agents.html +35 -35
- package/dist/docs/guides/convert-automation.html +35 -35
- package/dist/docs/guides/github.html +35 -35
- package/dist/docs/guides/gitlab.html +35 -35
- package/dist/docs/guides/grokbot-agents.html +35 -35
- package/dist/docs/guides/hooks.html +35 -35
- package/dist/docs/guides/improve.html +35 -35
- package/dist/docs/guides/jev.html +35 -35
- package/dist/docs/guides/mcp-oauth.html +35 -35
- package/dist/docs/guides/opentelemetry.html +35 -35
- package/dist/docs/guides/slack.html +39 -39
- package/dist/docs/guides/slack.md +16 -1
- package/dist/docs/guides/webhooks.html +35 -35
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +35 -35
- package/dist/docs/index.html +35 -35
- package/dist/docs/llms-full.txt +699 -752
- package/dist/docs/llms.txt +1 -1
- package/dist/docs/quickstart.html +35 -35
- package/dist/docs/reference/agent-config.html +35 -35
- package/dist/docs/reference/artifacts.html +35 -35
- package/dist/docs/reference/channels.html +35 -35
- package/dist/docs/reference/cli.html +127 -125
- package/dist/docs/reference/cli.md +685 -753
- package/dist/docs/reference/connections.html +35 -35
- package/dist/docs/reference/evals.html +35 -35
- package/dist/docs/reference/extensions.html +35 -35
- package/dist/docs/reference/hooks.html +35 -35
- package/dist/docs/reference/http-api.html +35 -35
- package/dist/docs/reference/instructions.html +35 -35
- package/dist/docs/reference/playground.html +35 -35
- package/dist/docs/reference/project-layout.html +35 -35
- package/dist/docs/reference/prompt.html +35 -35
- package/dist/docs/reference/schedules.html +35 -35
- package/dist/docs/reference/sessions.html +35 -35
- package/dist/docs/reference/skills.html +35 -35
- package/dist/docs/reference/subagents.html +35 -35
- package/dist/docs/reference/tools.html +35 -35
- package/dist/docs/templates/agentic-owners.html +35 -35
- package/dist/docs/templates/pr-autofixer.html +35 -35
- package/dist/docs/templates/security-reviewer.html +35 -35
- package/dist/docs/templates/thermo-quality-review.html +35 -35
- package/dist/docs/templates/thermo-review.html +35 -35
- package/dist/docs/templates/triage.html +35 -35
- package/dist/docs/troubleshooting.html +35 -35
- package/dist/files-backends/cursor-hosted.d.ts +11 -17
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +13 -41
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/artifacts-store.d.ts +11 -0
- package/dist/internal/artifacts-store.d.ts.map +1 -1
- package/dist/internal/artifacts-store.js +113 -18
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +9 -1
- package/dist/internal/cursor/cursor-api-transport.d.ts +37 -0
- package/dist/internal/cursor/cursor-api-transport.d.ts.map +1 -0
- package/dist/internal/cursor/cursor-api-transport.js +44 -0
- package/dist/internal/cursor/hosted-store-secrets.d.ts +21 -0
- package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
- package/dist/internal/cursor/hosted-store-secrets.js +26 -0
- package/dist/internal/cursor/store-api-client.d.ts +82 -0
- package/dist/internal/cursor/store-api-client.d.ts.map +1 -0
- package/dist/internal/cursor/store-api-client.js +227 -0
- package/dist/internal/deploy-client.d.ts +6 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-manifest.d.ts +8 -0
- package/dist/internal/deploy-manifest.d.ts.map +1 -1
- package/dist/internal/deploy-manifest.js +7 -1
- package/dist/internal/discovery/agent-config.d.ts +3 -1
- package/dist/internal/discovery/agent-config.d.ts.map +1 -1
- package/dist/internal/discovery/agent-config.js +7 -4
- package/dist/internal/framework-storage-selection.d.ts +1 -1
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/platform-timers.d.ts.map +1 -1
- package/dist/internal/platform-timers.js +20 -2
- package/dist/internal/reminder-runner.d.ts +79 -1
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +287 -46
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +7 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +13 -15
- package/dist/internal/store-api-protocol.d.ts +134 -0
- package/dist/internal/store-api-protocol.d.ts.map +1 -0
- package/dist/internal/store-api-protocol.js +126 -0
- package/dist/memory.d.ts +49 -10
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +193 -50
- package/dist/playground/assets/{index-CrMWlgUU.js → index-Cs0MKsv4.js} +30 -30
- package/dist/playground/assets/index-DLwnR9ys.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/reminders.d.ts +1 -1
- package/dist/reminders.d.ts.map +1 -1
- package/dist/types.d.ts +44 -10
- package/dist/types.d.ts.map +1 -1
- package/docs/guides/slack.md +16 -1
- package/docs/reference/cli.md +686 -754
- package/package.json +1 -1
- package/skills/setup-slack/SKILL.md +1 -1
- package/src/channels/slack/channel-watch.ts +57 -8
- package/src/channels/slack/inbound.ts +24 -0
- package/src/channels/slack/slack-channel.ts +127 -4
- package/src/channels/slack/types.ts +11 -12
- package/src/files-backends/cursor-hosted.ts +31 -68
- package/src/index.ts +1 -0
- package/src/internal/artifacts-store.ts +131 -25
- package/src/internal/cli-deploy.ts +12 -1
- package/src/internal/cursor/cursor-api-transport.ts +73 -0
- package/src/internal/cursor/hosted-store-secrets.ts +35 -0
- package/src/internal/cursor/store-api-client.ts +360 -0
- package/src/internal/deploy-client.ts +9 -0
- package/src/internal/deploy-manifest.ts +15 -0
- package/src/internal/discovery/agent-config.ts +8 -6
- package/src/internal/framework-storage-selection.ts +1 -1
- package/src/internal/platform-timers.ts +24 -2
- package/src/internal/reminder-runner.ts +454 -66
- package/src/internal/server.ts +8 -0
- package/src/internal/session-engine.ts +17 -22
- package/src/internal/store-api-protocol.ts +222 -0
- package/src/memory.ts +240 -59
- package/src/reminders.ts +1 -0
- package/src/types.ts +47 -10
- package/dist/docs/assets/chunks/@localSearchIndexroot.BnSgidYE.js +0 -1
- package/dist/docs/assets/chunks/channel.DdM5EfNW.js +0 -1
- package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.CjfGHeg2.js +0 -1
- package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.CjfGHeg2.js +0 -1
- package/dist/docs/assets/chunks/clone.wSOICb_f.js +0 -1
- package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.Cu5X28zZ.js +0 -1
- package/dist/docs/assets/chunks/wardley-RL74JXVD.DXy2i1LS.js +0 -162
- package/dist/docs/assets/reference_cli.md.DLWDz9ij.js +0 -95
- package/dist/docs/assets/reference_cli.md.DLWDz9ij.lean.js +0 -1
- package/dist/playground/assets/index-C61EWMBK.css +0 -1
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import type {
|
|
9
9
|
AuthContext,
|
|
10
|
+
ChannelSession,
|
|
10
11
|
DiscoveredReminder,
|
|
12
|
+
HostOtelApi,
|
|
11
13
|
JsonObject,
|
|
12
14
|
JsonValue,
|
|
13
15
|
ReminderCreateInput,
|
|
14
16
|
ReminderDefinition,
|
|
15
17
|
ReminderFireContext,
|
|
16
18
|
ReminderFireResult,
|
|
19
|
+
ReminderFollowupOptions,
|
|
17
20
|
ReminderInfo,
|
|
18
21
|
ReminderStatus,
|
|
19
22
|
ReminderUntilContext,
|
|
23
|
+
TurnOutcome,
|
|
20
24
|
} from "../types.js";
|
|
21
25
|
import {
|
|
22
26
|
channelContinuationIdentity,
|
|
@@ -47,6 +51,7 @@ import { APP_AUTH } from "./schedule-runner.js";
|
|
|
47
51
|
import {
|
|
48
52
|
type EngineSendOptions,
|
|
49
53
|
HTTP_CHANNEL_ID,
|
|
54
|
+
SessionAuthError,
|
|
50
55
|
SessionBusyError,
|
|
51
56
|
type SessionEngine,
|
|
52
57
|
} from "./session-engine.js";
|
|
@@ -87,6 +92,103 @@ export interface ReminderRunnerOptions {
|
|
|
87
92
|
controlPlane?: ReminderControlPlane;
|
|
88
93
|
/** Agent architecture; used with {@link controlPlane} for v2 protocol path. */
|
|
89
94
|
architecture?: "v1" | "v2";
|
|
95
|
+
/**
|
|
96
|
+
* Hosted v2 guest (`--no-control-plane`, architecture v2). A pod starts
|
|
97
|
+
* for one delivery and stops when it settles, so a fire usually lands on
|
|
98
|
+
* a pod that holds no session for the reminder's continuation, and the
|
|
99
|
+
* pod may stop as soon as the fire's outcome is applied. The runner then
|
|
100
|
+
* opens a session on the stored continuation key and waits for the turn.
|
|
101
|
+
*/
|
|
102
|
+
hostedGuest?: ReminderHostedGuestOptions;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ReminderHostedGuestOptions {
|
|
106
|
+
/**
|
|
107
|
+
* Principal for the session a prompt fire opens when the pod holds none.
|
|
108
|
+
* Hosted admissions open sessions with the same value, so a session the
|
|
109
|
+
* engine restores from durable storage keeps its principal. Return
|
|
110
|
+
* `undefined` when no principal applies; the fire then skips with a
|
|
111
|
+
* reschedule so the control plane keeps the reminder armed.
|
|
112
|
+
*/
|
|
113
|
+
fireAuth: () => AuthContext | null | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reschedule applied when a hosted fire cannot open a session (no
|
|
118
|
+
* principal). A one-shot `skipped` outcome without a reschedule completes
|
|
119
|
+
* the reminder, which would lose the check-in for good.
|
|
120
|
+
*/
|
|
121
|
+
export const REMINDER_SESSION_OPEN_RETRY_DELAY = "5m";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `reason` tag on `agent_sdk.reminder.fire_outcome`. Prompt fires report
|
|
125
|
+
* how the session was reached: `session_joined` (held by this pod),
|
|
126
|
+
* `session_restored` (read back from durable storage), `session_opened`
|
|
127
|
+
* (fresh session on the stored key), `session_gone` (no session and none
|
|
128
|
+
* could be opened). `fire_threw` pairs with kind `error`: the fire raised
|
|
129
|
+
* and the hosted admission retries it.
|
|
130
|
+
*/
|
|
131
|
+
export type ReminderFireOutcomeReason =
|
|
132
|
+
| "session_gone"
|
|
133
|
+
| "session_opened"
|
|
134
|
+
| "session_restored"
|
|
135
|
+
| "session_joined"
|
|
136
|
+
| "handler_missing"
|
|
137
|
+
| "handler_lost_on_restart"
|
|
138
|
+
| "missing_handler"
|
|
139
|
+
| "run_stop"
|
|
140
|
+
| "run_skip"
|
|
141
|
+
| "run_delivered"
|
|
142
|
+
| "until_fn"
|
|
143
|
+
| "busy"
|
|
144
|
+
| "followup_failed"
|
|
145
|
+
| "fire_threw";
|
|
146
|
+
|
|
147
|
+
export const REMINDER_FIRE_OUTCOME_METRIC = "agent_sdk.reminder.fire_outcome";
|
|
148
|
+
|
|
149
|
+
/** What one `executeFire` reports: the CP outcome plus the telemetry tags. */
|
|
150
|
+
interface FireExecution {
|
|
151
|
+
outcome: ReminderExecutionOutcome;
|
|
152
|
+
reason: ReminderFireOutcomeReason;
|
|
153
|
+
/** Set when the hosted guest awaited the turn before reporting. */
|
|
154
|
+
turnStatus?: TurnOutcome["status"];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface PromptDelivery {
|
|
158
|
+
result: ReminderFireResult;
|
|
159
|
+
reason: ReminderFireOutcomeReason;
|
|
160
|
+
turnStatus?: TurnOutcome["status"];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Out-params `sendFollowup` fills in for the fire's telemetry. */
|
|
164
|
+
interface FollowupSettle {
|
|
165
|
+
turnStatus?: TurnOutcome["status"];
|
|
166
|
+
skipReason?: Extract<ReminderFireOutcomeReason, "busy" | "followup_failed">;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** How a prompt fire reached (or failed to reach) its session. */
|
|
170
|
+
type FollowupTarget =
|
|
171
|
+
| {
|
|
172
|
+
auth: AuthContext | null;
|
|
173
|
+
reached: Extract<
|
|
174
|
+
ReminderFireOutcomeReason,
|
|
175
|
+
"session_joined" | "session_restored"
|
|
176
|
+
>;
|
|
177
|
+
}
|
|
178
|
+
| {
|
|
179
|
+
auth: undefined;
|
|
180
|
+
/**
|
|
181
|
+
* `session_gone`: no session anywhere. `session_unreadable`: the durable
|
|
182
|
+
* index has one but the local re-read missed; never open a fresh
|
|
183
|
+
* session over it.
|
|
184
|
+
*/
|
|
185
|
+
reached: "session_gone" | "session_unreadable";
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Guest-side read surface shared by v1 timed-work and the v2 CP client. */
|
|
189
|
+
interface ReminderReader {
|
|
190
|
+
getReminder(id: string): Promise<ReminderRecord | undefined>;
|
|
191
|
+
listReminders(): Promise<ReminderRecord[]>;
|
|
90
192
|
}
|
|
91
193
|
|
|
92
194
|
/**
|
|
@@ -169,6 +271,29 @@ function toInfo(record: ReminderRecord): ReminderInfo {
|
|
|
169
271
|
};
|
|
170
272
|
}
|
|
171
273
|
|
|
274
|
+
function stopFire(
|
|
275
|
+
reason: Extract<
|
|
276
|
+
ReminderFireOutcomeReason,
|
|
277
|
+
| "handler_missing"
|
|
278
|
+
| "handler_lost_on_restart"
|
|
279
|
+
| "missing_handler"
|
|
280
|
+
| "until_fn"
|
|
281
|
+
>
|
|
282
|
+
): FireExecution {
|
|
283
|
+
return { outcome: { kind: "stop", reason }, reason };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function runReason(result: ReminderFireResult): ReminderFireOutcomeReason {
|
|
287
|
+
switch (result.action) {
|
|
288
|
+
case "stop":
|
|
289
|
+
return "run_stop";
|
|
290
|
+
case "skip":
|
|
291
|
+
return "run_skip";
|
|
292
|
+
case "delivered":
|
|
293
|
+
return "run_delivered";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
172
297
|
function buildFirePrompt(record: ReminderRecord): string {
|
|
173
298
|
const recurring =
|
|
174
299
|
record.scheduleKind === "every" || record.scheduleKind === "cron";
|
|
@@ -205,6 +330,12 @@ export class ReminderRunner {
|
|
|
205
330
|
private readonly rejectAnonymousHandlers: boolean;
|
|
206
331
|
private readonly controlPlane: ReminderControlPlane | undefined;
|
|
207
332
|
private readonly architecture: "v1" | "v2" | undefined;
|
|
333
|
+
private readonly hostedGuest: ReminderHostedGuestOptions | undefined;
|
|
334
|
+
/**
|
|
335
|
+
* A control-plane read failure is logged once per outage, not per call.
|
|
336
|
+
* The next successful read re-arms the log.
|
|
337
|
+
*/
|
|
338
|
+
private controlPlaneReadFailureLogged = false;
|
|
208
339
|
|
|
209
340
|
constructor(
|
|
210
341
|
private readonly engine: SessionEngine,
|
|
@@ -215,6 +346,7 @@ export class ReminderRunner {
|
|
|
215
346
|
this.rejectAnonymousHandlers = options.rejectAnonymousHandlers === true;
|
|
216
347
|
this.controlPlane = options.controlPlane;
|
|
217
348
|
this.architecture = options.architecture;
|
|
349
|
+
this.hostedGuest = options.hostedGuest;
|
|
218
350
|
for (const reminder of options.namedHandlers ?? []) {
|
|
219
351
|
this.namedHandlers.set(reminder.name, reminder.definition);
|
|
220
352
|
this.namedHandlerNames.set(reminder.definition, reminder.name);
|
|
@@ -226,29 +358,62 @@ export class ReminderRunner {
|
|
|
226
358
|
return this.controlPlane !== undefined && this.architecture === "v2";
|
|
227
359
|
}
|
|
228
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Where reminder documents are read from. Under the CP protocol that is
|
|
363
|
+
* the v2 control-plane client; the v1 timed-work route answers 405 for a
|
|
364
|
+
* v2 deployment, which used to leave `list()` with only this process's
|
|
365
|
+
* in-memory map (breaking arm idempotence and `cancelForContinuation`).
|
|
366
|
+
*/
|
|
367
|
+
private get reminderReader(): ReminderReader | undefined {
|
|
368
|
+
if (this.usesControlPlaneProtocol) {
|
|
369
|
+
return this.controlPlane;
|
|
370
|
+
}
|
|
371
|
+
return this.platformTimers;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private logControlPlaneReadFailure(what: string, error: unknown): void {
|
|
375
|
+
if (this.usesControlPlaneProtocol && this.controlPlaneReadFailureLogged) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.controlPlaneReadFailureLogged = true;
|
|
379
|
+
this.logger(
|
|
380
|
+
`[agent-sdk] reminders: control-plane ${what} failed: ${
|
|
381
|
+
error instanceof Error ? error.message : String(error)
|
|
382
|
+
}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
229
386
|
/**
|
|
230
387
|
* Load reminder documents from the control plane into memory. Does not
|
|
231
388
|
* arm in-process timers — the CP owns clocks.
|
|
232
389
|
*/
|
|
233
390
|
async hydrate(): Promise<void> {
|
|
234
|
-
|
|
391
|
+
const reader = this.reminderReader;
|
|
392
|
+
if (reader === undefined) {
|
|
235
393
|
return;
|
|
236
394
|
}
|
|
237
395
|
let remote: ReminderRecord[] = [];
|
|
238
396
|
try {
|
|
239
|
-
remote = await
|
|
397
|
+
remote = await reader.listReminders();
|
|
240
398
|
} catch (error) {
|
|
241
|
-
this.
|
|
242
|
-
`[agent-sdk] reminders: control-plane list failed: ${
|
|
243
|
-
error instanceof Error ? error.message : String(error)
|
|
244
|
-
}`
|
|
245
|
-
);
|
|
399
|
+
this.logControlPlaneReadFailure("list", error);
|
|
246
400
|
return;
|
|
247
401
|
}
|
|
402
|
+
this.controlPlaneReadFailureLogged = false;
|
|
248
403
|
this.records.clear();
|
|
249
404
|
for (const record of remote) {
|
|
250
405
|
this.records.set(record.id, record);
|
|
251
406
|
}
|
|
407
|
+
if (this.usesControlPlaneProtocol) {
|
|
408
|
+
// The CP owns status; a fire whose handler is missing reports `stop`
|
|
409
|
+
// from executeFire instead of the guest writing a disarm.
|
|
410
|
+
if (remote.length > 0) {
|
|
411
|
+
this.logger(
|
|
412
|
+
`[agent-sdk] reminders: hydrated ${remote.length} reminder(s) from control plane`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
252
417
|
let disarmedOrphan = 0;
|
|
253
418
|
for (const record of [...this.records.values()]) {
|
|
254
419
|
if (record.status !== "armed") {
|
|
@@ -330,9 +495,11 @@ export class ReminderRunner {
|
|
|
330
495
|
}
|
|
331
496
|
|
|
332
497
|
private async getRecord(id: string): Promise<ReminderRecord | undefined> {
|
|
333
|
-
|
|
498
|
+
const reader = this.reminderReader;
|
|
499
|
+
if (reader !== undefined) {
|
|
334
500
|
try {
|
|
335
|
-
const remote = await
|
|
501
|
+
const remote = await reader.getReminder(id);
|
|
502
|
+
this.controlPlaneReadFailureLogged = false;
|
|
336
503
|
if (remote === undefined) {
|
|
337
504
|
this.records.delete(id);
|
|
338
505
|
return undefined;
|
|
@@ -340,31 +507,33 @@ export class ReminderRunner {
|
|
|
340
507
|
this.records.set(id, remote);
|
|
341
508
|
return remote;
|
|
342
509
|
} catch (error) {
|
|
343
|
-
this.
|
|
344
|
-
`[agent-sdk] reminders: control-plane get failed for ${id}: ${
|
|
345
|
-
error instanceof Error ? error.message : String(error)
|
|
346
|
-
}`
|
|
347
|
-
);
|
|
510
|
+
this.logControlPlaneReadFailure(`get for ${id}`, error);
|
|
348
511
|
return this.records.get(id);
|
|
349
512
|
}
|
|
350
513
|
}
|
|
351
514
|
return this.records.get(id);
|
|
352
515
|
}
|
|
353
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Under the CP protocol a list failure propagates: the in-memory map is
|
|
519
|
+
* only what this process created, and returning it would let a caller
|
|
520
|
+
* arm duplicates or miss rows to cancel. v1 keeps the best-effort form.
|
|
521
|
+
*/
|
|
354
522
|
private async allRecords(): Promise<ReminderRecord[]> {
|
|
355
|
-
|
|
523
|
+
const reader = this.reminderReader;
|
|
524
|
+
if (reader !== undefined) {
|
|
356
525
|
try {
|
|
357
|
-
const remote = await
|
|
526
|
+
const remote = await reader.listReminders();
|
|
527
|
+
this.controlPlaneReadFailureLogged = false;
|
|
358
528
|
this.records.clear();
|
|
359
529
|
for (const record of remote) {
|
|
360
530
|
this.records.set(record.id, record);
|
|
361
531
|
}
|
|
362
532
|
} catch (error) {
|
|
363
|
-
this.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
);
|
|
533
|
+
this.logControlPlaneReadFailure("list", error);
|
|
534
|
+
if (this.usesControlPlaneProtocol) {
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
368
537
|
}
|
|
369
538
|
}
|
|
370
539
|
return [...this.records.values()];
|
|
@@ -697,25 +866,46 @@ export class ReminderRunner {
|
|
|
697
866
|
): Promise<ReminderExecutionOutcome> {
|
|
698
867
|
const record = this.recordFromDescriptor(descriptor);
|
|
699
868
|
this.records.set(record.id, record);
|
|
869
|
+
let fire: FireExecution;
|
|
870
|
+
try {
|
|
871
|
+
fire = await this.runFire(record);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
// The hosted admission maps the throw to a retryable failure; count
|
|
874
|
+
// it so the retry loop is visible beside the settled outcomes.
|
|
875
|
+
this.recordFireOutcome(record, {
|
|
876
|
+
kind: "error",
|
|
877
|
+
reason: "fire_threw",
|
|
878
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
879
|
+
});
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
this.recordFireOutcome(record, {
|
|
883
|
+
kind: fire.outcome.kind,
|
|
884
|
+
reason: fire.reason,
|
|
885
|
+
turnStatus: fire.turnStatus,
|
|
886
|
+
});
|
|
887
|
+
return fire.outcome;
|
|
888
|
+
}
|
|
700
889
|
|
|
890
|
+
private async runFire(record: ReminderRecord): Promise<FireExecution> {
|
|
701
891
|
const live = this.handlers.get(record.id) ?? {};
|
|
702
892
|
if (record.handler !== undefined) {
|
|
703
893
|
if (this.resolveNamedHandler(record.handler) === undefined) {
|
|
704
|
-
return
|
|
894
|
+
return stopFire("handler_missing");
|
|
705
895
|
}
|
|
706
896
|
} else if (
|
|
707
897
|
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
708
898
|
live.run === undefined &&
|
|
709
899
|
live.untilFn === undefined
|
|
710
900
|
) {
|
|
711
|
-
return
|
|
901
|
+
return stopFire("handler_lost_on_restart");
|
|
712
902
|
}
|
|
713
903
|
|
|
714
904
|
if (live.untilFn !== undefined) {
|
|
715
905
|
try {
|
|
716
906
|
const done = await live.untilFn(this.buildUntilContext(record));
|
|
717
907
|
if (done) {
|
|
718
|
-
return
|
|
908
|
+
return stopFire("until_fn");
|
|
719
909
|
}
|
|
720
910
|
} catch (error) {
|
|
721
911
|
this.logger(
|
|
@@ -728,50 +918,94 @@ export class ReminderRunner {
|
|
|
728
918
|
|
|
729
919
|
if (await this.isSessionBusy(record.channelId, record.continuationToken)) {
|
|
730
920
|
if (record.busyPolicy === "skipIfBusy") {
|
|
731
|
-
|
|
732
|
-
`[agent-sdk] reminder "${record.id}" (${record.purpose}) skipped: session busy`
|
|
733
|
-
);
|
|
734
|
-
return { kind: "skipped" };
|
|
921
|
+
return { outcome: { kind: "skipped" }, reason: "busy" };
|
|
735
922
|
}
|
|
736
923
|
await this.whenSessionIdle(record.channelId, record.continuationToken);
|
|
737
924
|
}
|
|
738
925
|
|
|
739
926
|
let result: ReminderFireResult;
|
|
927
|
+
let reason: ReminderFireOutcomeReason;
|
|
928
|
+
let turnStatus: TurnOutcome["status"] | undefined;
|
|
740
929
|
if (record.handler !== undefined) {
|
|
741
930
|
const named = this.resolveNamedHandler(record.handler);
|
|
742
931
|
if (named === undefined) {
|
|
743
|
-
return
|
|
932
|
+
return stopFire("handler_missing");
|
|
744
933
|
}
|
|
745
934
|
result = await named.run(this.buildFireContext(record));
|
|
935
|
+
reason = runReason(result);
|
|
746
936
|
} else if (live.run !== undefined) {
|
|
747
937
|
result = await live.run(this.buildFireContext(record));
|
|
938
|
+
reason = runReason(result);
|
|
748
939
|
} else if (record.prompt !== undefined) {
|
|
749
|
-
|
|
940
|
+
const delivery = await this.deliverPromptFire(record);
|
|
941
|
+
result = delivery.result;
|
|
942
|
+
reason = delivery.reason;
|
|
943
|
+
turnStatus = delivery.turnStatus;
|
|
750
944
|
} else {
|
|
751
|
-
return
|
|
945
|
+
return stopFire("missing_handler");
|
|
752
946
|
}
|
|
753
947
|
|
|
754
948
|
if (result.action === "stop") {
|
|
755
|
-
return { kind: "stop", reason
|
|
756
|
-
}
|
|
757
|
-
if (result.action === "skip") {
|
|
758
|
-
return {
|
|
759
|
-
kind: "skipped",
|
|
760
|
-
reschedule:
|
|
761
|
-
result.reschedule !== undefined
|
|
762
|
-
? toWireReschedule(result.reschedule)
|
|
763
|
-
: undefined,
|
|
764
|
-
};
|
|
949
|
+
return { outcome: { kind: "stop", reason }, reason };
|
|
765
950
|
}
|
|
951
|
+
const reschedule =
|
|
952
|
+
result.reschedule !== undefined
|
|
953
|
+
? toWireReschedule(result.reschedule)
|
|
954
|
+
: undefined;
|
|
766
955
|
return {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
956
|
+
outcome: {
|
|
957
|
+
kind: result.action === "skip" ? "skipped" : "delivered",
|
|
958
|
+
reschedule,
|
|
959
|
+
},
|
|
960
|
+
reason,
|
|
961
|
+
turnStatus,
|
|
772
962
|
};
|
|
773
963
|
}
|
|
774
964
|
|
|
965
|
+
/**
|
|
966
|
+
* One structured line and one `agent_sdk.reminder.fire_outcome` count per
|
|
967
|
+
* fire. Emitted through the host OTel meter, the same path hosted v2
|
|
968
|
+
* exports turn/tool metrics on, so a fire that ends in `stop` is visible
|
|
969
|
+
* beside the control plane's `timed_work.fire` count.
|
|
970
|
+
*/
|
|
971
|
+
private recordFireOutcome(
|
|
972
|
+
record: ReminderRecord,
|
|
973
|
+
fire: {
|
|
974
|
+
kind: ReminderExecutionOutcome["kind"] | "error";
|
|
975
|
+
reason: ReminderFireOutcomeReason;
|
|
976
|
+
turnStatus?: TurnOutcome["status"];
|
|
977
|
+
detail?: string;
|
|
978
|
+
}
|
|
979
|
+
): void {
|
|
980
|
+
this.logger(
|
|
981
|
+
`[agent-sdk] reminder fire_outcome kind=${fire.kind} reason=${fire.reason}` +
|
|
982
|
+
` reminderId=${record.id} purpose=${record.purpose}` +
|
|
983
|
+
` channelId=${record.channelId} fireCount=${record.fireCount}` +
|
|
984
|
+
(fire.turnStatus === undefined ? "" : ` turn=${fire.turnStatus}`) +
|
|
985
|
+
(fire.detail === undefined
|
|
986
|
+
? ""
|
|
987
|
+
: ` detail=${JSON.stringify(fire.detail)}`)
|
|
988
|
+
);
|
|
989
|
+
// Test doubles may omit `host.otel`; the metric is best-effort.
|
|
990
|
+
const otel: HostOtelApi | undefined = this.engine.host.otel;
|
|
991
|
+
if (otel === undefined) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
otel.increment(REMINDER_FIRE_OUTCOME_METRIC, {
|
|
996
|
+
kind: fire.kind,
|
|
997
|
+
reason: fire.reason,
|
|
998
|
+
purpose: record.purpose,
|
|
999
|
+
});
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
this.logger(
|
|
1002
|
+
`[agent-sdk] reminder fire_outcome metric failed: ${
|
|
1003
|
+
error instanceof Error ? error.message : String(error)
|
|
1004
|
+
}`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
775
1009
|
private recordFromDescriptor(
|
|
776
1010
|
descriptor: ReminderFireDescriptor
|
|
777
1011
|
): ReminderRecord {
|
|
@@ -1033,19 +1267,121 @@ export class ReminderRunner {
|
|
|
1033
1267
|
private async deliverPrompt(
|
|
1034
1268
|
record: ReminderRecord
|
|
1035
1269
|
): Promise<ReminderFireResult> {
|
|
1270
|
+
return (await this.deliverPromptFire(record)).result;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Where a prompt fire joins. The pod's own store first; on a hosted guest
|
|
1275
|
+
* the durable mirror next, because the pod that armed the reminder is
|
|
1276
|
+
* usually gone and the session lives only in storage. A restored session
|
|
1277
|
+
* keeps its own principal (channel admissions may carry a sender), so the
|
|
1278
|
+
* fire joins under that, never under {@link ReminderHostedGuestOptions.fireAuth}.
|
|
1279
|
+
*/
|
|
1280
|
+
private async resolveFollowupTarget(
|
|
1281
|
+
record: ReminderRecord
|
|
1282
|
+
): Promise<FollowupTarget> {
|
|
1283
|
+
const local = await this.resolveFollowupAuth(
|
|
1284
|
+
record.channelId,
|
|
1285
|
+
record.continuationToken
|
|
1286
|
+
);
|
|
1287
|
+
if (local !== undefined) {
|
|
1288
|
+
return { auth: local, reached: "session_joined" };
|
|
1289
|
+
}
|
|
1290
|
+
if (this.hostedGuest === undefined) {
|
|
1291
|
+
return { auth: undefined, reached: "session_gone" };
|
|
1292
|
+
}
|
|
1293
|
+
// Materializes the durable record locally; a storage error propagates
|
|
1294
|
+
// and the hosted admission retries the fire.
|
|
1295
|
+
const restored = await this.engine.hasContinuationSession(
|
|
1296
|
+
record.channelId,
|
|
1297
|
+
record.continuationToken
|
|
1298
|
+
);
|
|
1299
|
+
if (!restored) {
|
|
1300
|
+
return { auth: undefined, reached: "session_gone" };
|
|
1301
|
+
}
|
|
1036
1302
|
const auth = await this.resolveFollowupAuth(
|
|
1037
1303
|
record.channelId,
|
|
1038
1304
|
record.continuationToken
|
|
1039
1305
|
);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1306
|
+
return auth === undefined
|
|
1307
|
+
? { auth: undefined, reached: "session_unreadable" }
|
|
1308
|
+
: { auth, reached: "session_restored" };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
private async deliverPromptFire(
|
|
1312
|
+
record: ReminderRecord
|
|
1313
|
+
): Promise<PromptDelivery> {
|
|
1314
|
+
const target = await this.resolveFollowupTarget(record);
|
|
1315
|
+
if (target.auth !== undefined) {
|
|
1316
|
+
return this.promptDelivery(record, target.auth, target.reached);
|
|
1317
|
+
}
|
|
1318
|
+
if (target.reached === "session_unreadable") {
|
|
1319
|
+
this.logger(
|
|
1320
|
+
`[agent-sdk] reminder "${record.id}" (${record.purpose}) durable session did not materialize locally; not opening a new one`
|
|
1321
|
+
);
|
|
1322
|
+
return { result: { action: "stop" }, reason: "session_gone" };
|
|
1323
|
+
}
|
|
1324
|
+
if (
|
|
1325
|
+
this.hostedGuest !== undefined &&
|
|
1326
|
+
this.channelUsesControlPlaneContinuation(record.channelId)
|
|
1327
|
+
) {
|
|
1328
|
+
// No session on this pod and none in durable storage. Open one on the
|
|
1329
|
+
// stored key the way an inbound event on a fresh key does, instead of
|
|
1330
|
+
// consuming the fire with no turn.
|
|
1331
|
+
const fireAuth = this.hostedGuest.fireAuth();
|
|
1332
|
+
if (fireAuth === undefined) {
|
|
1333
|
+
return {
|
|
1334
|
+
result: {
|
|
1335
|
+
action: "skip",
|
|
1336
|
+
reschedule: { delay: REMINDER_SESSION_OPEN_RETRY_DELAY },
|
|
1337
|
+
},
|
|
1338
|
+
reason: "session_gone",
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
return await this.promptDelivery(record, fireAuth, "session_opened");
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (!(error instanceof SessionAuthError)) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
// A session landed on this key under another principal between the
|
|
1348
|
+
// lookup and the send. That does not change on retry, so stop
|
|
1349
|
+
// rather than let the control plane re-admit until its attempt cap.
|
|
1350
|
+
this.logger(
|
|
1351
|
+
`[agent-sdk] reminder "${record.id}" (${record.purpose}) cannot join session on its continuation: ${error.message}`
|
|
1352
|
+
);
|
|
1353
|
+
return { result: { action: "stop" }, reason: "session_gone" };
|
|
1045
1354
|
}
|
|
1046
|
-
return { action: "stop" };
|
|
1047
1355
|
}
|
|
1048
|
-
|
|
1356
|
+
// Under CP protocol, return stop and let the CP apply/CAS; do not
|
|
1357
|
+
// mutate authoritative status from the guest mid-fire.
|
|
1358
|
+
if (!this.usesControlPlaneProtocol) {
|
|
1359
|
+
await this.disarm(record, "session_gone");
|
|
1360
|
+
}
|
|
1361
|
+
return { result: { action: "stop" }, reason: "session_gone" };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
private async promptDelivery(
|
|
1365
|
+
record: ReminderRecord,
|
|
1366
|
+
auth: AuthContext | null,
|
|
1367
|
+
delivered: Extract<
|
|
1368
|
+
ReminderFireOutcomeReason,
|
|
1369
|
+
"session_joined" | "session_restored" | "session_opened"
|
|
1370
|
+
>
|
|
1371
|
+
): Promise<PromptDelivery> {
|
|
1372
|
+
const settle: FollowupSettle = {};
|
|
1373
|
+
const result = await this.sendFollowup(
|
|
1374
|
+
record,
|
|
1375
|
+
buildFirePrompt(record),
|
|
1376
|
+
auth,
|
|
1377
|
+
settle
|
|
1378
|
+
);
|
|
1379
|
+
return {
|
|
1380
|
+
result,
|
|
1381
|
+
reason:
|
|
1382
|
+
result.action === "skip" ? (settle.skipReason ?? "busy") : delivered,
|
|
1383
|
+
turnStatus: settle.turnStatus,
|
|
1384
|
+
};
|
|
1049
1385
|
}
|
|
1050
1386
|
|
|
1051
1387
|
/**
|
|
@@ -1086,23 +1422,56 @@ export class ReminderRunner {
|
|
|
1086
1422
|
return continuationSource(identity) === "control-plane";
|
|
1087
1423
|
}
|
|
1088
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Local and v1 track the turn in the background: the process outlives the
|
|
1427
|
+
* fire. A hosted v2 guest waits for the turn and its durable writes first,
|
|
1428
|
+
* because the control plane may stop the pod as soon as the fire's
|
|
1429
|
+
* outcome is applied, and `delivered` must mean the turn ran.
|
|
1430
|
+
*/
|
|
1431
|
+
private async settleFollowup(
|
|
1432
|
+
session: ChannelSession,
|
|
1433
|
+
settle: FollowupSettle | undefined
|
|
1434
|
+
): Promise<void> {
|
|
1435
|
+
const completion = this.engine.waitForCompletion(session.id);
|
|
1436
|
+
if (this.hostedGuest === undefined) {
|
|
1437
|
+
this.engine.trackBackground(completion);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const outcome = await completion;
|
|
1441
|
+
await this.engine.drainSessionDurability(session.id);
|
|
1442
|
+
if (settle !== undefined) {
|
|
1443
|
+
settle.turnStatus = outcome.status;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1089
1447
|
private async sendFollowup(
|
|
1090
1448
|
record: ReminderRecord,
|
|
1091
1449
|
message: string,
|
|
1092
|
-
auth: AuthContext | null
|
|
1450
|
+
auth: AuthContext | null,
|
|
1451
|
+
settle?: FollowupSettle,
|
|
1452
|
+
extra?: ReminderFollowupOptions
|
|
1093
1453
|
): Promise<ReminderFireResult> {
|
|
1094
|
-
const sendOptions =
|
|
1454
|
+
const sendOptions: EngineSendOptions = {
|
|
1455
|
+
...this.followupSendOptions(record, auth),
|
|
1456
|
+
state: extra?.state,
|
|
1457
|
+
refreshState: extra?.refreshState,
|
|
1458
|
+
title: extra?.title,
|
|
1459
|
+
clearCloudOverride: extra?.clearCloudOverride,
|
|
1460
|
+
};
|
|
1095
1461
|
try {
|
|
1096
1462
|
const session = await this.engine.send(
|
|
1097
1463
|
record.channelId,
|
|
1098
1464
|
message,
|
|
1099
1465
|
sendOptions
|
|
1100
1466
|
);
|
|
1101
|
-
this.
|
|
1467
|
+
await this.settleFollowup(session, settle);
|
|
1102
1468
|
return { action: "delivered" };
|
|
1103
1469
|
} catch (error) {
|
|
1104
1470
|
if (error instanceof SessionBusyError) {
|
|
1105
1471
|
if (record.busyPolicy === "skipIfBusy") {
|
|
1472
|
+
if (settle !== undefined) {
|
|
1473
|
+
settle.skipReason = "busy";
|
|
1474
|
+
}
|
|
1106
1475
|
return { action: "skip" };
|
|
1107
1476
|
}
|
|
1108
1477
|
await this.whenSessionIdle(record.channelId, record.continuationToken);
|
|
@@ -1112,9 +1481,7 @@ export class ReminderRunner {
|
|
|
1112
1481
|
message,
|
|
1113
1482
|
sendOptions
|
|
1114
1483
|
);
|
|
1115
|
-
this.
|
|
1116
|
-
this.engine.waitForCompletion(session.id)
|
|
1117
|
-
);
|
|
1484
|
+
await this.settleFollowup(session, settle);
|
|
1118
1485
|
return { action: "delivered" };
|
|
1119
1486
|
} catch (retryError) {
|
|
1120
1487
|
this.logger(
|
|
@@ -1124,6 +1491,9 @@ export class ReminderRunner {
|
|
|
1124
1491
|
: String(retryError)
|
|
1125
1492
|
}`
|
|
1126
1493
|
);
|
|
1494
|
+
if (settle !== undefined) {
|
|
1495
|
+
settle.skipReason = "followup_failed";
|
|
1496
|
+
}
|
|
1127
1497
|
return { action: "skip" };
|
|
1128
1498
|
}
|
|
1129
1499
|
}
|
|
@@ -1147,18 +1517,36 @@ export class ReminderRunner {
|
|
|
1147
1517
|
mcp: this.engine.host.mcp,
|
|
1148
1518
|
appAuth: APP_AUTH,
|
|
1149
1519
|
artifacts: this.engine.artifacts,
|
|
1150
|
-
async followup(args: { message: string }) {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1520
|
+
async followup(args: { message: string } & ReminderFollowupOptions) {
|
|
1521
|
+
const target = await self.resolveFollowupTarget(record);
|
|
1522
|
+
let auth = target.auth;
|
|
1523
|
+
// `openAuth` opens only when nothing exists anywhere: an unreadable
|
|
1524
|
+
// durable session must not be shadowed by a second one on the same
|
|
1525
|
+
// key (prompt-fire parity).
|
|
1526
|
+
if (
|
|
1527
|
+
auth === undefined &&
|
|
1528
|
+
target.reached === "session_gone" &&
|
|
1529
|
+
args.openAuth !== undefined
|
|
1530
|
+
) {
|
|
1531
|
+
auth = args.openAuth;
|
|
1532
|
+
}
|
|
1155
1533
|
if (auth === undefined) {
|
|
1156
|
-
|
|
1534
|
+
// Under the CP protocol the guest never writes reminder status;
|
|
1535
|
+
// the fire's `stop` outcome carries the reason instead.
|
|
1536
|
+
if (!self.usesControlPlaneProtocol) {
|
|
1537
|
+
await self.disarm(record, "session_gone");
|
|
1538
|
+
}
|
|
1157
1539
|
throw new Error(
|
|
1158
1540
|
`Reminder "${record.id}" followup: session gone for continuation`
|
|
1159
1541
|
);
|
|
1160
1542
|
}
|
|
1161
|
-
const result = await self.sendFollowup(
|
|
1543
|
+
const result = await self.sendFollowup(
|
|
1544
|
+
record,
|
|
1545
|
+
args.message,
|
|
1546
|
+
auth,
|
|
1547
|
+
undefined,
|
|
1548
|
+
args
|
|
1549
|
+
);
|
|
1162
1550
|
if (result.action === "skip") {
|
|
1163
1551
|
throw new Error(
|
|
1164
1552
|
`Reminder "${record.id}" followup skipped: session busy`
|