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