@cursor/july 0.2.0 → 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/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -4
- 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.9eAtsjAM.js → app.D23Y-7Tp.js} +4 -4
- package/dist/docs/assets/chunks/@localSearchIndexroot.CzCCM7N8.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.C9s69nxj.js → VPLocalSearchBox.CWBeTFRZ.js} +1 -1
- package/dist/docs/assets/chunks/{arc.DmWDRaF-.js → arc.DSF2O3pm.js} +1 -1
- package/dist/docs/assets/chunks/{architectureDiagram-Q4EWVU46.BE1Wj4f3.js → architectureDiagram-Q4EWVU46.J52Wzbkg.js} +1 -1
- package/dist/docs/assets/chunks/{baseUniq.pmEZGnWu.js → baseUniq.CQS3LPCt.js} +1 -1
- package/dist/docs/assets/chunks/{blockDiagram-DXYQGD6D.D6mM-5XN.js → blockDiagram-DXYQGD6D.Dw339Gr5.js} +1 -1
- package/dist/docs/assets/chunks/{c4Diagram-AHTNJAMY.ZQYTPC1b.js → c4Diagram-AHTNJAMY.BUdtOaRZ.js} +1 -1
- package/dist/docs/assets/chunks/channel.Bfu4df88.js +1 -0
- package/dist/docs/assets/chunks/{chunk-4BX2VUAB.CKJ7gaK8.js → chunk-4BX2VUAB.CuOrkEqk.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-4TB4RGXK.CL61HnvB.js → chunk-4TB4RGXK.BJNBcY7U.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-55IACEB6.DsA4XA1m.js → chunk-55IACEB6.VJK5LAm_.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-EDXVE4YY.BODHCHFW.js → chunk-EDXVE4YY.BYYLihvj.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-FMBD7UC4.D6NBaZTa.js → chunk-FMBD7UC4.CmoW8BXP.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-OYMX7WX6.BLPuqFXg.js → chunk-OYMX7WX6.DTGY4C-M.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-QZHKN3VN.BFs_ML8Z.js → chunk-QZHKN3VN.Cg5n67vl.js} +1 -1
- package/dist/docs/assets/chunks/{chunk-YZCP3GAM.BDjuFyZv.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.D85HNEsi.js → cose-bilkent-S5V4N54A.BTRG8N3b.js} +1 -1
- package/dist/docs/assets/chunks/{dagre-KV5264BT.V463KlSF.js → dagre-KV5264BT.Bob_bp_p.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-5BDNPKRD.DhBDu1ab.js → diagram-5BDNPKRD.ggPcs9uO.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-G4DWMVQ6.v6sC68zl.js → diagram-G4DWMVQ6.BP0qyJkp.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-MMDJMWI5.DhHsdXYv.js → diagram-MMDJMWI5.B0X24UKr.js} +1 -1
- package/dist/docs/assets/chunks/{diagram-TYMM5635.BPWNSFZc.js → diagram-TYMM5635.B4rXHFVt.js} +1 -1
- package/dist/docs/assets/chunks/{erDiagram-SMLLAGMA.Cq0wTMPL.js → erDiagram-SMLLAGMA._55Rt9oX.js} +1 -1
- package/dist/docs/assets/chunks/{flowDiagram-DWJPFMVM.MveMtucC.js → flowDiagram-DWJPFMVM.DGP4XvR5.js} +1 -1
- package/dist/docs/assets/chunks/{ganttDiagram-T4ZO3ILL.BzKfGUSf.js → ganttDiagram-T4ZO3ILL.BtXtkL4E.js} +1 -1
- package/dist/docs/assets/chunks/{gitGraphDiagram-UUTBAWPF.OToXTSW_.js → gitGraphDiagram-UUTBAWPF.B9cPWblK.js} +1 -1
- package/dist/docs/assets/chunks/{graph.D82tam-l.js → graph.D8HzNexS.js} +1 -1
- package/dist/docs/assets/chunks/{infoDiagram-42DDH7IO.DzFlRmcE.js → infoDiagram-42DDH7IO.Bw7CQUpi.js} +1 -1
- package/dist/docs/assets/chunks/{ishikawaDiagram-UXIWVN3A.CVPRJiKe.js → ishikawaDiagram-UXIWVN3A.MwkzF6nQ.js} +1 -1
- package/dist/docs/assets/chunks/{journeyDiagram-VCZTEJTY.CQvNTfQC.js → journeyDiagram-VCZTEJTY.DIGFF-3C.js} +1 -1
- package/dist/docs/assets/chunks/{kanban-definition-6JOO6SKY.BwywywUl.js → kanban-definition-6JOO6SKY.DhYef2BN.js} +1 -1
- package/dist/docs/assets/chunks/{layout.C4BkPPba.js → layout.C0XUxuPi.js} +1 -1
- package/dist/docs/assets/chunks/{linear.FoSfGKD4.js → linear.BwNPpZex.js} +1 -1
- package/dist/docs/assets/chunks/{min.yLh8jqfl.js → min.CwAQdL7z.js} +1 -1
- package/dist/docs/assets/chunks/{mindmap-definition-QFDTVHPH.C9Dq2_CN.js → mindmap-definition-QFDTVHPH.pWsSVLsP.js} +1 -1
- package/dist/docs/assets/chunks/{pieDiagram-DEJITSTG.CiGyBcES.js → pieDiagram-DEJITSTG.BDJ3FbBy.js} +1 -1
- package/dist/docs/assets/chunks/{quadrantDiagram-34T5L4WZ.1FWea9nj.js → quadrantDiagram-34T5L4WZ.Co80izyB.js} +1 -1
- package/dist/docs/assets/chunks/{requirementDiagram-MS252O5E.B1q7ntiV.js → requirementDiagram-MS252O5E.JveKw4yx.js} +1 -1
- package/dist/docs/assets/chunks/{sankeyDiagram-XADWPNL6.Cpr4oU_k.js → sankeyDiagram-XADWPNL6.B0A7adPi.js} +1 -1
- package/dist/docs/assets/chunks/{sequenceDiagram-FGHM5R23.CUK68STn.js → sequenceDiagram-FGHM5R23.d6JZ5Hre.js} +1 -1
- package/dist/docs/assets/chunks/{stateDiagram-FHFEXIEX.zU0yL2m4.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.DhnKd0CD.js → theme.MJTLx0hh.js} +2 -2
- package/dist/docs/assets/chunks/{timeline-definition-GMOUNBTQ.CNYoXo3F.js → timeline-definition-GMOUNBTQ.CFS7Ai4c.js} +1 -1
- package/dist/docs/assets/chunks/{vennDiagram-DHZGUBPP.DFWH3G9s.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.7WLkqM7s.js → wardleyDiagram-NUSXRM2D.B_8mvtjh.js} +1 -1
- package/dist/docs/assets/chunks/{xychartDiagram-5P7HB3ND.-SyUNgWw.js → xychartDiagram-5P7HB3ND.DtjU5H85.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.DVjNyqq5.js → guides_slack.md.Bjw2r2gL.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.DVjNyqq5.lean.js → guides_slack.md.Bjw2r2gL.lean.js} +1 -1
- 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 +15 -7
- 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 +15 -7
- 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 +35 -35
- 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/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-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/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +13 -4
- 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 -14
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +193 -74
- package/dist/playground/assets/{index-CX6oTKwz.js → index-Cs0MKsv4.js} +30 -30
- package/dist/playground/index.html +1 -1
- package/dist/reminders.d.ts +1 -1
- package/dist/reminders.d.ts.map +1 -1
- package/dist/types.d.ts +28 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/guides/slack.md +15 -7
- 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/slack-channel.ts +4 -3
- 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/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-manifest.ts +15 -0
- package/src/internal/reminder-runner.ts +30 -5
- package/src/internal/session-engine.ts +17 -22
- package/src/internal/store-api-protocol.ts +222 -0
- package/src/memory.ts +240 -93
- package/src/reminders.ts +1 -0
- package/src/types.ts +31 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DjNBgxTF.js +0 -1
- package/dist/docs/assets/chunks/channel.BNF8VK-B.js +0 -1
- package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.iViHzfrK.js +0 -1
- package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.iViHzfrK.js +0 -1
- package/dist/docs/assets/chunks/clone.CJoR2BBM.js +0 -1
- package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.nV8Nl-td.js +0 -1
- package/dist/docs/assets/chunks/wardley-RL74JXVD.Do4PwzeN.js +0 -162
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-call wire contract for the factory store API:
|
|
3
|
+
* `POST /internal/agentsdk/store/api/<call>` carrying a versioned JSON
|
|
4
|
+
* envelope `{ v, call, payload }`. This is the lane where the **server** owns
|
|
5
|
+
* the storage pattern (journal sharding, rotation, compaction), so a
|
|
6
|
+
* pathological pattern is fixable in a backend deploy instead of a July
|
|
7
|
+
* release plus a rebake of every deployment. The verb lane (`/flush`,
|
|
8
|
+
* `/read`, `/list`) keeps carrying bulk presigned file IO.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately duplicated on the backend
|
|
11
|
+
* (`backend/server/src/factory/agent-sdk/agentsdkStoreApiProtocol.ts`): this
|
|
12
|
+
* package is published to npm, so the backend cannot depend on it and it
|
|
13
|
+
* cannot depend on backend packages. A backend drift test compares the two
|
|
14
|
+
* sources — same shape as the hosted-delivery protocol twins — which is why
|
|
15
|
+
* every exported constant here stays a single `export const NAME = <literal>;`
|
|
16
|
+
* line and every schema field stays on its own line.
|
|
17
|
+
*
|
|
18
|
+
* Evolution rules (the compat contract): additive optional fields only; no
|
|
19
|
+
* renames, no retypes, no semantic reuse of an old field; unknown fields are
|
|
20
|
+
* ignored, never rejected; removing a field means deprecating it in place
|
|
21
|
+
* forever. A breaking change means a new `v`, and the server keeps serving
|
|
22
|
+
* every previous `v`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
|
|
27
|
+
export const STORE_API_PROTOCOL_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
/** Path family on the factory surface; one segment per call. */
|
|
30
|
+
export const STORE_API_PATH = "/internal/agentsdk/store/api";
|
|
31
|
+
|
|
32
|
+
export const STORE_API_MEMORY_APPEND_CALL = "memory.append";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Semantic cap on one serialized memory record. Text fields are truncated
|
|
36
|
+
* client-side (`memoryHook` default 2000 chars per field), so a well-formed
|
|
37
|
+
* record is a few KiB; 16 KiB is generous headroom, not a target. The cap is
|
|
38
|
+
* part of the wire contract but rides the layers, not the schema: the
|
|
39
|
+
* backend's store controller enforces it as a typed 4xx, and the store API
|
|
40
|
+
* client (`cursor/store-api-client.ts`) pre-checks it before spending a
|
|
41
|
+
* doomed request.
|
|
42
|
+
*/
|
|
43
|
+
export const MEMORY_APPEND_MAX_RECORD_BYTES: number = 16 * 1024;
|
|
44
|
+
|
|
45
|
+
/** Serialized size of a record, as counted against the byte cap. */
|
|
46
|
+
export function serializedRecordBytes(record: unknown): number {
|
|
47
|
+
return new TextEncoder().encode(JSON.stringify(record)).byteLength;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One turn's memory record as it crosses the wire — July's
|
|
52
|
+
* `TurnMemoryRecord` (memory.ts). The extra-key record mirrors the schema's
|
|
53
|
+
* `.passthrough()`: a newer client's additive optional fields are journaled
|
|
54
|
+
* verbatim rather than stripped by an older server.
|
|
55
|
+
*/
|
|
56
|
+
export type TurnMemoryRecordWire = {
|
|
57
|
+
at: string;
|
|
58
|
+
sessionId: string;
|
|
59
|
+
channelId: string;
|
|
60
|
+
status: "completed" | "failed";
|
|
61
|
+
title?: string;
|
|
62
|
+
sdkAgentId?: string;
|
|
63
|
+
userMessage?: string;
|
|
64
|
+
result?: string;
|
|
65
|
+
usage?: Record<string, unknown>;
|
|
66
|
+
} & Record<string, unknown>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wire schema for {@link TurnMemoryRecordWire}. Per-field caps are
|
|
70
|
+
* deliberately absent: the client truncates text at authoring time and
|
|
71
|
+
* {@link MEMORY_APPEND_MAX_RECORD_BYTES} bounds the whole record.
|
|
72
|
+
*/
|
|
73
|
+
export const turnMemoryRecordWireSchema: z.ZodType<TurnMemoryRecordWire> = z
|
|
74
|
+
.object({
|
|
75
|
+
at: z.string().min(1),
|
|
76
|
+
sessionId: z.string().min(1),
|
|
77
|
+
channelId: z.string().min(1),
|
|
78
|
+
status: z.enum(["completed", "failed"]),
|
|
79
|
+
title: z.string().optional(),
|
|
80
|
+
sdkAgentId: z.string().optional(),
|
|
81
|
+
userMessage: z.string().optional(),
|
|
82
|
+
result: z.string().optional(),
|
|
83
|
+
usage: z.object({}).passthrough().optional(),
|
|
84
|
+
})
|
|
85
|
+
.passthrough();
|
|
86
|
+
|
|
87
|
+
/** `memory.append` validated payload. */
|
|
88
|
+
export type MemoryAppendPayload = {
|
|
89
|
+
agent: string;
|
|
90
|
+
record: TurnMemoryRecordWire;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `memory.append` payload shape. The agent name becomes a store key segment
|
|
95
|
+
* on the server, so it must be exactly one segment. Unknown payload fields
|
|
96
|
+
* are stripped (= ignored), per the evolution rules. The byte cap is
|
|
97
|
+
* semantics, not shape — the backend's store controller owns it.
|
|
98
|
+
*/
|
|
99
|
+
export const memoryAppendPayloadSchema: z.ZodType<MemoryAppendPayload> =
|
|
100
|
+
z.object({
|
|
101
|
+
agent: z
|
|
102
|
+
.string()
|
|
103
|
+
.min(1)
|
|
104
|
+
.max(256)
|
|
105
|
+
.refine(
|
|
106
|
+
name =>
|
|
107
|
+
!name.includes("/") &&
|
|
108
|
+
!name.includes("\\") &&
|
|
109
|
+
name !== "." &&
|
|
110
|
+
name !== "..",
|
|
111
|
+
{ message: "agent must be a single store key segment" }
|
|
112
|
+
),
|
|
113
|
+
record: turnMemoryRecordWireSchema,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** The versioned request body for the `memory.append` call. */
|
|
117
|
+
export type MemoryAppendEnvelope = {
|
|
118
|
+
v: typeof STORE_API_PROTOCOL_VERSION;
|
|
119
|
+
call: typeof STORE_API_MEMORY_APPEND_CALL;
|
|
120
|
+
payload: MemoryAppendPayload;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The whole request body. Unknown envelope fields are stripped (= ignored). */
|
|
124
|
+
export const memoryAppendEnvelopeSchema: z.ZodType<MemoryAppendEnvelope> =
|
|
125
|
+
z.object({
|
|
126
|
+
v: z.literal(STORE_API_PROTOCOL_VERSION),
|
|
127
|
+
call: z.literal(STORE_API_MEMORY_APPEND_CALL),
|
|
128
|
+
payload: memoryAppendPayloadSchema,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** `memory.append` success body. */
|
|
132
|
+
export type MemoryAppendResponse = {
|
|
133
|
+
v: typeof STORE_API_PROTOCOL_VERSION;
|
|
134
|
+
ok: true;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Success body. Failures answer the store surface's standard error JSON
|
|
139
|
+
* (`{ code, error }`) with a matching HTTP status.
|
|
140
|
+
*/
|
|
141
|
+
export const memoryAppendResponseSchema: z.ZodType<MemoryAppendResponse> =
|
|
142
|
+
z.object({
|
|
143
|
+
v: z.literal(STORE_API_PROTOCOL_VERSION),
|
|
144
|
+
ok: z.literal(true),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const STORE_API_MEMORY_READ_CALL = "memory.read";
|
|
148
|
+
|
|
149
|
+
/** `memory.read` validated payload. */
|
|
150
|
+
export type MemoryReadPayload = {
|
|
151
|
+
agent: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `memory.read` payload shape — the same single-segment agent name rule as
|
|
156
|
+
* `memory.append`, since the name addresses the same journal key family.
|
|
157
|
+
*/
|
|
158
|
+
export const memoryReadPayloadSchema: z.ZodType<MemoryReadPayload> = z.object({
|
|
159
|
+
agent: z
|
|
160
|
+
.string()
|
|
161
|
+
.min(1)
|
|
162
|
+
.max(256)
|
|
163
|
+
.refine(
|
|
164
|
+
name =>
|
|
165
|
+
!name.includes("/") &&
|
|
166
|
+
!name.includes("\\") &&
|
|
167
|
+
name !== "." &&
|
|
168
|
+
name !== "..",
|
|
169
|
+
{ message: "agent must be a single store key segment" }
|
|
170
|
+
),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/** The versioned request body for the `memory.read` call. */
|
|
174
|
+
export type MemoryReadEnvelope = {
|
|
175
|
+
v: typeof STORE_API_PROTOCOL_VERSION;
|
|
176
|
+
call: typeof STORE_API_MEMORY_READ_CALL;
|
|
177
|
+
payload: MemoryReadPayload;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** The whole request body. Unknown envelope fields are stripped (= ignored). */
|
|
181
|
+
export const memoryReadEnvelopeSchema: z.ZodType<MemoryReadEnvelope> = z.object(
|
|
182
|
+
{
|
|
183
|
+
v: z.literal(STORE_API_PROTOCOL_VERSION),
|
|
184
|
+
call: z.literal(STORE_API_MEMORY_READ_CALL),
|
|
185
|
+
payload: memoryReadPayloadSchema,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `memory.read` success body. The call is flush-on-read: the server drains
|
|
191
|
+
* the agent's pending append buffer before presigning. Best-effort: when
|
|
192
|
+
* the drain succeeds the journal includes every append that preceded the
|
|
193
|
+
* read; a busy flush lock, a buffer outage, or an oversized backlog
|
|
194
|
+
* degrades to the already-flushed state (callers must not regress their
|
|
195
|
+
* local view on a shorter download). `journal` is null while the agent has
|
|
196
|
+
* no journal at all.
|
|
197
|
+
*/
|
|
198
|
+
export type MemoryReadResponse = {
|
|
199
|
+
v: typeof STORE_API_PROTOCOL_VERSION;
|
|
200
|
+
ok: true;
|
|
201
|
+
journal: {
|
|
202
|
+
url: string;
|
|
203
|
+
expiresAt: string;
|
|
204
|
+
} | null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Success body. Failures answer the store surface's standard error JSON
|
|
209
|
+
* (`{ code, error }`) with a matching HTTP status.
|
|
210
|
+
*/
|
|
211
|
+
export const memoryReadResponseSchema: z.ZodType<MemoryReadResponse> = z.object(
|
|
212
|
+
{
|
|
213
|
+
v: z.literal(STORE_API_PROTOCOL_VERSION),
|
|
214
|
+
ok: z.literal(true),
|
|
215
|
+
journal: z
|
|
216
|
+
.object({
|
|
217
|
+
url: z.string().min(1),
|
|
218
|
+
expiresAt: z.string().min(1),
|
|
219
|
+
})
|
|
220
|
+
.nullable(),
|
|
221
|
+
}
|
|
222
|
+
);
|
package/src/memory.ts
CHANGED
|
@@ -18,20 +18,29 @@
|
|
|
18
18
|
*
|
|
19
19
|
* On Cursor-managed hosting the default is {@link agentStoreMemoryBackend}:
|
|
20
20
|
* the journal lives on the deployment's Agent Store (survives deploys,
|
|
21
|
-
* visible on cloud VMs under the store mount)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* visible on cloud VMs under the store mount), written exclusively through
|
|
22
|
+
* the `memory.append` domain call — the server owns the journal pattern —
|
|
23
|
+
* and mirrored to `<stateRoot>/memory/journal.jsonl` (hydrated per session
|
|
24
|
+
* via `memory.read`, appended locally per turn) so the workspace symlink
|
|
25
|
+
* read path keeps working for local-runtime turns. The
|
|
26
|
+
* `factory-api-v1` pin file exists only on hosted pods — plain local dev
|
|
27
|
+
* runs {@link fileMemoryBackend} and never sees the hosted lane.
|
|
24
28
|
*/
|
|
25
29
|
|
|
26
30
|
import { randomUUID } from "node:crypto";
|
|
27
31
|
import { appendFile, mkdir, rename, stat, writeFile } from "node:fs/promises";
|
|
28
32
|
import { join } from "node:path";
|
|
29
|
-
import { agentStoreKeys
|
|
33
|
+
import { agentStoreKeys } from "./files.js";
|
|
30
34
|
import {
|
|
31
|
-
|
|
35
|
+
type CursorHostedFilesOptions,
|
|
32
36
|
isCursorHostedFilesAvailable,
|
|
33
37
|
} from "./files-backends/cursor-hosted.js";
|
|
34
38
|
import { defineHook } from "./hooks.js";
|
|
39
|
+
import {
|
|
40
|
+
createStoreApiClient,
|
|
41
|
+
isStoreApiLaneEnabled,
|
|
42
|
+
type StoreApiClient,
|
|
43
|
+
} from "./internal/cursor/store-api-client.js";
|
|
35
44
|
import type { HookContext, HookDefinition, TurnUsage } from "./types.js";
|
|
36
45
|
|
|
37
46
|
/** Name of the shared memory directory under the agent state root. */
|
|
@@ -61,6 +70,13 @@ export interface MemoryBackend {
|
|
|
61
70
|
record: TurnMemoryRecord,
|
|
62
71
|
ctx: { stateRoot: string; agentName: string }
|
|
63
72
|
): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Optional session-start hook: bring the backend's local read path up to
|
|
75
|
+
* date before the session's first turn reads it. A freshness upgrade, not
|
|
76
|
+
* a correctness gate — implementations soft-fail and keep whatever read
|
|
77
|
+
* state already exists.
|
|
78
|
+
*/
|
|
79
|
+
prepareSession?(ctx: { stateRoot: string; agentName: string }): Promise<void>;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
export interface FileMemoryBackendOptions {
|
|
@@ -113,120 +129,233 @@ export function fileMemoryBackend(
|
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
export interface AgentStoreMemoryBackendOptions {
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Local mirror rotation threshold, same meaning as
|
|
134
|
+
* {@link FileMemoryBackendOptions}. The durable journal's rotation is the
|
|
135
|
+
* server's (`memory.append` controller), not this.
|
|
136
|
+
*/
|
|
119
137
|
maxJournalBytes?: number;
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Jittered exponential backoff between CAS attempts. Retrying immediately is
|
|
135
|
-
* a fleet-wide stampede: every losing pod re-GETs the full journal and
|
|
136
|
-
* re-PUTs in lockstep, which multiplied reads ~65x on one deployment during
|
|
137
|
-
* the 2026-09-18 Agent Store read storm and tripped the store's
|
|
138
|
-
* per-service-account read limit (every pod shares the deployment
|
|
139
|
-
* credential). Jitter desynchronizes the writers so most retries land on the
|
|
140
|
-
* first or second attempt.
|
|
141
|
-
*/
|
|
142
|
-
function memoryCasBackoffMs(attempt: number, random: () => number): number {
|
|
143
|
-
return 100 * 2 ** attempt * (0.5 + random());
|
|
138
|
+
/**
|
|
139
|
+
* Cursor-hosted transport overrides (tests): base URL, credential, store
|
|
140
|
+
* source id, protocol pin, fetch. Feeds the default
|
|
141
|
+
* {@link storeApiClient} and the lane-availability check.
|
|
142
|
+
*/
|
|
143
|
+
hosted?: CursorHostedFilesOptions;
|
|
144
|
+
/**
|
|
145
|
+
* Domain-lane client override (tests). Defaults to
|
|
146
|
+
* {@link createStoreApiClient} over {@link hosted} — the module that owns
|
|
147
|
+
* the `memory.append` / `memory.read` transport, typed errors, and
|
|
148
|
+
* bounded backoff.
|
|
149
|
+
*/
|
|
150
|
+
storeApiClient?: StoreApiClient;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
154
|
* Journal on the deployment's Agent Store — durable across deploys, visible
|
|
148
|
-
* on cloud VMs under the store mount
|
|
149
|
-
*
|
|
150
|
-
*
|
|
155
|
+
* on cloud VMs under the store mount, written exclusively through the
|
|
156
|
+
* `memory.append` domain call: the server owns the journal's storage
|
|
157
|
+
* pattern (Redis-buffered flush, rotation), so a pathological pattern is
|
|
158
|
+
* fixable in a backend deploy instead of a July release plus a rebake.
|
|
159
|
+
*
|
|
160
|
+
* There is no client-composed fallback. The read-modify-write CAS lane that
|
|
161
|
+
* predated the domain call (and caused the 2026-09-18 Agent Store read
|
|
162
|
+
* storm) was removed in 0.2.1 together with v2 hosting binding the
|
|
163
|
+
* `factory-api-v1` pin unconditionally and refusing artifacts frozen on an
|
|
164
|
+
* older July. A hosted pod without the pin is therefore a misconfiguration
|
|
165
|
+
* (a server predating the pin change, or a local env naming a hosted store
|
|
166
|
+
* without one): records are dropped loudly, never written client-side.
|
|
151
167
|
*/
|
|
152
168
|
export function agentStoreMemoryBackend(
|
|
153
169
|
options: AgentStoreMemoryBackendOptions = {}
|
|
154
170
|
): MemoryBackend {
|
|
155
|
-
const
|
|
171
|
+
const hosted = options.hosted ?? {};
|
|
156
172
|
const maxJournalBytes = options.maxJournalBytes ?? 5 * 1024 * 1024;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const random = options.random ?? Math.random;
|
|
161
|
-
// Appends are chained per journal key (same shape as fileMemoryBackend);
|
|
162
|
-
// a failed append must not poison the chain for later turns. The chain
|
|
163
|
-
// serializes appends within this pod only — pods race each other on the
|
|
164
|
-
// shared key, which is what the CAS retry (with backoff) absorbs.
|
|
173
|
+
// Mirror work is chained per journal key (same shape as
|
|
174
|
+
// fileMemoryBackend): hydration and appends on one agent's mirror never
|
|
175
|
+
// interleave, and a failed step must not poison the chain for later turns.
|
|
165
176
|
const appendChains = new Map<string, Promise<void>>();
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
// Transport, envelope, typed errors, and bounded backoff all live in the
|
|
179
|
+
// client; this module only decides which lane an append takes.
|
|
180
|
+
const storeApiClient = options.storeApiClient ?? createStoreApiClient(hosted);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The domain lane still owes local-runtime sessions their read path:
|
|
184
|
+
* cloud-runtime turns read the journal through the store mount, but
|
|
185
|
+
* local-runtime turns on hosted pods (the default `runtime: "local"`)
|
|
186
|
+
* read `<stateRoot>/memory/journal.jsonl` via the workspace symlink, and
|
|
187
|
+
* on hosting the only writer of that file is this backend. So the lane
|
|
188
|
+
* keeps the mirror live without reintroducing the CAS read pattern:
|
|
189
|
+
* hydrate it once from the store when the file is absent (a fresh pod
|
|
190
|
+
* after a deploy), then append each successfully posted record locally,
|
|
191
|
+
* rotating at {@link maxJournalBytes} like {@link fileMemoryBackend}.
|
|
192
|
+
* Server-buffered records not yet flushed when a pod hydrates are the
|
|
193
|
+
* lane's documented staleness window (~30 s / 64 KiB), and other pods'
|
|
194
|
+
* appends surface on the next hydration — advisory memory, same envelope
|
|
195
|
+
* as the server side.
|
|
196
|
+
*/
|
|
197
|
+
const hydratedMirrors = new Set<string>();
|
|
198
|
+
let mirrorRotationSeq = 0;
|
|
199
|
+
const maintainLocalMirror = async (
|
|
168
200
|
record: TurnMemoryRecord,
|
|
169
201
|
ctx: { stateRoot: string; agentName: string }
|
|
170
202
|
): Promise<void> => {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
current === undefined || current instanceof Uint8Array
|
|
183
|
-
? undefined
|
|
184
|
-
: current.etag;
|
|
185
|
-
let next: Uint8Array;
|
|
186
|
-
let rotatedStamp: string | undefined;
|
|
187
|
-
if (currentBody.byteLength >= maxJournalBytes) {
|
|
188
|
-
rotatedStamp = `${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
189
|
-
await sink.put(
|
|
190
|
-
agentStoreKeys.memoryJournalRotated(ctx.agentName, rotatedStamp),
|
|
191
|
-
currentBody,
|
|
192
|
-
{ ifMatch: null }
|
|
193
|
-
);
|
|
194
|
-
next = line;
|
|
195
|
-
} else {
|
|
196
|
-
next = Buffer.concat([currentBody, line]);
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
await sink.put(key, next, {
|
|
200
|
-
ifMatch: current === undefined ? null : etag,
|
|
203
|
+
const dir = join(ctx.stateRoot, MEMORY_DIR_NAME);
|
|
204
|
+
const path = join(dir, "journal.jsonl");
|
|
205
|
+
if (!hydratedMirrors.has(path)) {
|
|
206
|
+
// Appends are chained per journal key, so this guard never races
|
|
207
|
+
// itself for one agent. Backstop for a session that appended before
|
|
208
|
+
// any prepareSession hydration ran (e.g. a custom hook ordering).
|
|
209
|
+
hydratedMirrors.add(path);
|
|
210
|
+
const existing = await stat(path).catch(() => undefined);
|
|
211
|
+
if (existing === undefined) {
|
|
212
|
+
const content = await storeApiClient.memoryReadContent({
|
|
213
|
+
agent: ctx.agentName,
|
|
201
214
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
error instanceof FileConflictError &&
|
|
205
|
-
attempt < MEMORY_CAS_MAX_ATTEMPTS - 1
|
|
206
|
-
) {
|
|
207
|
-
await sleep(memoryCasBackoffMs(attempt, random));
|
|
208
|
-
continue;
|
|
215
|
+
if (content !== undefined) {
|
|
216
|
+
await mirrorJournalLocally(ctx.stateRoot, content);
|
|
209
217
|
}
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
await mirrorJournalLocally(ctx.stateRoot, next);
|
|
213
|
-
if (rotatedStamp !== undefined) {
|
|
214
|
-
// Rotated segments sit beside the live journal locally too, so the
|
|
215
|
-
// workspace symlink read path keeps pre-rotation history.
|
|
216
|
-
await writeFile(
|
|
217
|
-
join(ctx.stateRoot, MEMORY_DIR_NAME, `journal-${rotatedStamp}.jsonl`),
|
|
218
|
-
currentBody
|
|
219
|
-
);
|
|
220
218
|
}
|
|
219
|
+
}
|
|
220
|
+
await mkdir(dir, { recursive: true });
|
|
221
|
+
const size = (await stat(path).catch(() => undefined))?.size ?? 0;
|
|
222
|
+
if (size >= maxJournalBytes) {
|
|
223
|
+
mirrorRotationSeq += 1;
|
|
224
|
+
await rename(
|
|
225
|
+
path,
|
|
226
|
+
join(dir, `journal-${Date.now()}-${mirrorRotationSeq}.jsonl`)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* One `memory.append` domain call; the server appends to the server-owned
|
|
234
|
+
* journal. On success the record is also appended to the local mirror so
|
|
235
|
+
* the workspace symlink read path stays live (see
|
|
236
|
+
* {@link maintainLocalMirror}).
|
|
237
|
+
*
|
|
238
|
+
* Failure policy: log and give up for this record. Memory is advisory and
|
|
239
|
+
* the hook runner treats an append failure as non-fatal to the turn;
|
|
240
|
+
* falling back to the CAS path per-record would reintroduce the write
|
|
241
|
+
* storm under exactly the server brownout that makes this call fail. A
|
|
242
|
+
* failed POST also skips the mirror — the mirror must never show a record
|
|
243
|
+
* the durable journal will not have.
|
|
244
|
+
*/
|
|
245
|
+
const appendViaStoreApi = async (
|
|
246
|
+
record: TurnMemoryRecord,
|
|
247
|
+
ctx: { stateRoot: string; agentName: string }
|
|
248
|
+
): Promise<void> => {
|
|
249
|
+
try {
|
|
250
|
+
await storeApiClient.memoryAppend({ agent: ctx.agentName, record });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.warn(
|
|
253
|
+
`[agent-serve] memory.append dropped one record for "${ctx.agentName}": ${
|
|
254
|
+
error instanceof Error ? error.message : String(error)
|
|
255
|
+
}`
|
|
256
|
+
);
|
|
221
257
|
return;
|
|
222
258
|
}
|
|
259
|
+
try {
|
|
260
|
+
await maintainLocalMirror(record, ctx);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.warn(
|
|
263
|
+
`[agent-serve] memory mirror update failed for "${ctx.agentName}" (journal record is durable): ${
|
|
264
|
+
error instanceof Error ? error.message : String(error)
|
|
265
|
+
}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Session-start hydration: one `memory.read` (flush-on-read server-side,
|
|
272
|
+
* so the pending buffer — including a burst another pod appended — lands
|
|
273
|
+
* first) and the mirror is rewritten to global state; the session then
|
|
274
|
+
* reads everything appended before it started. Soft-fail: a failed
|
|
275
|
+
* hydration keeps the existing mirror.
|
|
276
|
+
*/
|
|
277
|
+
const hydrateMirror = async (ctx: {
|
|
278
|
+
stateRoot: string;
|
|
279
|
+
agentName: string;
|
|
280
|
+
}): Promise<void> => {
|
|
281
|
+
const path = join(ctx.stateRoot, MEMORY_DIR_NAME, "journal.jsonl");
|
|
282
|
+
try {
|
|
283
|
+
const content = await storeApiClient.memoryReadContent({
|
|
284
|
+
agent: ctx.agentName,
|
|
285
|
+
});
|
|
286
|
+
if (content !== undefined) {
|
|
287
|
+
// Never regress the read path: every mirrored record was
|
|
288
|
+
// server-acked before it was written locally, so a download SHORTER
|
|
289
|
+
// than the mirror means the server's flush-on-read was degraded (a
|
|
290
|
+
// busy lock, a Redis outage — the acked tail is still buffered) or
|
|
291
|
+
// the journal rotated. Keeping the richer mirror loses nothing;
|
|
292
|
+
// rewriting would hide records the agent has already seen.
|
|
293
|
+
const mirroredBytes =
|
|
294
|
+
(await stat(path).catch(() => undefined))?.size ?? 0;
|
|
295
|
+
if (content.byteLength >= mirroredBytes) {
|
|
296
|
+
await mirrorJournalLocally(ctx.stateRoot, content);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Either way the read answered: skip the append path's absent-file
|
|
300
|
+
// hydration for the rest of this process.
|
|
301
|
+
hydratedMirrors.add(path);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.warn(
|
|
304
|
+
`[agent-serve] memory hydration failed for "${ctx.agentName}" (keeping the existing mirror): ${
|
|
305
|
+
error instanceof Error ? error.message : String(error)
|
|
306
|
+
}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Hosted pods carry the pin and a provisioned store by construction (v2
|
|
313
|
+
* hosting binds `factory-api-v1` unconditionally and refuses pre-0.2.1
|
|
314
|
+
* artifacts). Reaching this without them means a misconfigured
|
|
315
|
+
* environment; there is no client-side write to fall back to.
|
|
316
|
+
*/
|
|
317
|
+
const refuseMisconfiguredLane = (agentName: string, verb: string): void => {
|
|
318
|
+
console.error(
|
|
319
|
+
`[agent-serve] memory ${verb} for "${agentName}" dropped: the store API lane is unavailable (missing/foreign AGENT_SERVE_STORE_PROTOCOL pin or no provisioned store). This July has no client-side journal fallback; fix the hosting pin.`
|
|
320
|
+
);
|
|
223
321
|
};
|
|
224
322
|
|
|
225
323
|
return {
|
|
324
|
+
async prepareSession(ctx) {
|
|
325
|
+
// Chained on the same per-journal key as appends, so hydration never
|
|
326
|
+
// interleaves with an append's mirror write.
|
|
327
|
+
const key = agentStoreKeys.memoryJournal(ctx.agentName);
|
|
328
|
+
const prior = appendChains.get(key) ?? Promise.resolve();
|
|
329
|
+
const next = prior
|
|
330
|
+
.catch(() => {})
|
|
331
|
+
.then(async () => {
|
|
332
|
+
if (!isStoreApiLaneEnabled(hosted)) {
|
|
333
|
+
refuseMisconfiguredLane(ctx.agentName, "hydration");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await hydrateMirror(ctx);
|
|
337
|
+
});
|
|
338
|
+
appendChains.set(key, next);
|
|
339
|
+
await next;
|
|
340
|
+
},
|
|
341
|
+
|
|
226
342
|
async appendTurn(record, ctx) {
|
|
343
|
+
// Soft-fail by construction (appendViaStoreApi never throws), but
|
|
344
|
+
// still chained per journal key: the local mirror's hydrate-then-
|
|
345
|
+
// append must not interleave with itself. Lane availability is
|
|
346
|
+
// re-resolved lazily per append, so a pin bound after serve start
|
|
347
|
+
// takes effect without a restart.
|
|
227
348
|
const key = agentStoreKeys.memoryJournal(ctx.agentName);
|
|
228
349
|
const prior = appendChains.get(key) ?? Promise.resolve();
|
|
229
|
-
const next = prior
|
|
350
|
+
const next = prior
|
|
351
|
+
.catch(() => {})
|
|
352
|
+
.then(async () => {
|
|
353
|
+
if (!isStoreApiLaneEnabled(hosted)) {
|
|
354
|
+
refuseMisconfiguredLane(ctx.agentName, "append");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
await appendViaStoreApi(record, ctx);
|
|
358
|
+
});
|
|
230
359
|
appendChains.set(key, next);
|
|
231
360
|
await next;
|
|
232
361
|
},
|
|
@@ -334,6 +463,24 @@ export function memoryHook(options: MemoryHookOptions = {}): HookDefinition {
|
|
|
334
463
|
|
|
335
464
|
return defineHook({
|
|
336
465
|
events: {
|
|
466
|
+
async "session.started"(_event, ctx) {
|
|
467
|
+
// Bring the mirror (the workspace symlink read path) up to date
|
|
468
|
+
// before the session's first turn reads it. Backends without a
|
|
469
|
+
// prepareSession (plain files) have nothing to freshen. Soft-fail:
|
|
470
|
+
// hydration is a freshness upgrade, never a turn blocker.
|
|
471
|
+
try {
|
|
472
|
+
await backend.prepareSession?.({
|
|
473
|
+
stateRoot: ctx.stateRoot,
|
|
474
|
+
agentName: ctx.agent.name,
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.warn(
|
|
478
|
+
`[agent-serve] memory hydration at session start failed: ${
|
|
479
|
+
error instanceof Error ? error.message : String(error)
|
|
480
|
+
}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
337
484
|
async "message.received"(event, ctx) {
|
|
338
485
|
pendingMessages.set(
|
|
339
486
|
pendingKey(ctx, event.turnId),
|
package/src/reminders.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -3000,6 +3000,34 @@ export interface ReminderUntilContext {
|
|
|
3000
3000
|
mcp: HostMcpRegistry;
|
|
3001
3001
|
}
|
|
3002
3002
|
|
|
3003
|
+
/**
|
|
3004
|
+
* Send fields a reminder followup may carry onto the turn it starts, the
|
|
3005
|
+
* subset of {@link SendMessageOptions} a host pipeline needs when the fire
|
|
3006
|
+
* itself prepares the turn (channel state for a later `defineResult.commit`,
|
|
3007
|
+
* a session title, a cloud-attach reset). Continuation addressing stays the
|
|
3008
|
+
* reminder's stored binding and is not overridable here.
|
|
3009
|
+
*/
|
|
3010
|
+
export interface ReminderFollowupOptions {
|
|
3011
|
+
/** Initial channel state when the followup creates the session. */
|
|
3012
|
+
state?: JsonValue;
|
|
3013
|
+
/** Shallow-merged into an existing session's channel state (see {@link SendMessageOptions.refreshState}). */
|
|
3014
|
+
refreshState?: JsonValue;
|
|
3015
|
+
/** Session display title (applied when creating a new session). */
|
|
3016
|
+
title?: string;
|
|
3017
|
+
/** See {@link SendMessageOptions.clearCloudOverride}. */
|
|
3018
|
+
clearCloudOverride?: boolean;
|
|
3019
|
+
/**
|
|
3020
|
+
* Open a new session under this principal when the stored continuation
|
|
3021
|
+
* has none anywhere (pod or durable storage) — the session an inbound
|
|
3022
|
+
* delivery on that key would create, so later deliveries under the same
|
|
3023
|
+
* principal keep joining it. A durable session that exists but cannot be
|
|
3024
|
+
* materialized still throws, matching prompt fires: opening over it would
|
|
3025
|
+
* shadow the record. Omitted, a missing session also keeps the existing
|
|
3026
|
+
* behavior: the followup throws.
|
|
3027
|
+
*/
|
|
3028
|
+
openAuth?: AuthContext;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3003
3031
|
export interface ReminderFireContext extends ReminderUntilContext {
|
|
3004
3032
|
appAuth: AuthContext;
|
|
3005
3033
|
/**
|
|
@@ -3013,7 +3041,9 @@ export interface ReminderFireContext extends ReminderUntilContext {
|
|
|
3013
3041
|
* completes, because the pod may stop once the fire's outcome is applied;
|
|
3014
3042
|
* locally and on v1 it resolves as soon as the turn is admitted.
|
|
3015
3043
|
*/
|
|
3016
|
-
followup(
|
|
3044
|
+
followup(
|
|
3045
|
+
args: { message: string } & ReminderFollowupOptions
|
|
3046
|
+
): Promise<ChannelSession>;
|
|
3017
3047
|
cancel(reason?: string): Promise<void>;
|
|
3018
3048
|
/**
|
|
3019
3049
|
* Durable artifacts (unbound — the reminder is bound to a continuation
|