@stackstackstack/dsh-goal 0.1.5

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/lib/index.js ADDED
@@ -0,0 +1,827 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { z as z$1 } from "zod";
4
+ import { agentEvents } from "@stackstackstack/dsh-agent";
5
+ import { Remote, TypertRemoteService } from "@stackstackstack/dsh-typert-protocol";
6
+ import { HarnessError } from "@stackstackstack/dsh-llm";
7
+ //#region lib/types/runtime.js
8
+ /** Runtime constructors and protocol constants for the goal domain. */
9
+ /** Version of the goal change embedded in a round-zero message source. */
10
+ const GOAL_CHANGE_VERSION = 1;
11
+ /**
12
+ * Brand a string as a goal id.
13
+ * @param id - raw goal identifier.
14
+ * @returns the same string with the compile-time brand.
15
+ */
16
+ function GoalId(id) {
17
+ return id;
18
+ }
19
+ /** Error returned by the goal domain boundary. */
20
+ var GoalError = class extends HarnessError {
21
+ /**
22
+ * @param message - human-readable rejection reason.
23
+ * @param code - stable machine-routable classification.
24
+ */
25
+ constructor(message, code) {
26
+ super(message, code);
27
+ }
28
+ };
29
+ //#endregion
30
+ //#region lib/types/fold.js
31
+ /** Pure replay fold and strict decoder for durable goal changes. */
32
+ const SNAPSHOT_OPERATIONS = new Set([
33
+ "create",
34
+ "edit",
35
+ "pause",
36
+ "resume",
37
+ "complete",
38
+ "block"
39
+ ]);
40
+ const PHASES = new Set([
41
+ "active",
42
+ "paused",
43
+ "blocked",
44
+ "complete"
45
+ ]);
46
+ /**
47
+ * Build an empty replay accumulator.
48
+ * @returns mutable state with no current goal or prior ref.
49
+ */
50
+ function emptyGoalFoldState() {
51
+ return {
52
+ goal: void 0,
53
+ roundsStarted: 0,
54
+ createdAt: void 0,
55
+ updatedAt: void 0,
56
+ lastRef: void 0,
57
+ seenGoalIds: /* @__PURE__ */ new Set()
58
+ };
59
+ }
60
+ /** Whether a value is a JSON record rather than an array. */
61
+ function isRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ /** Require one positive safe integer. */
65
+ function positiveInteger(value, field) {
66
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) throw new Error(`goal change ${field} must be a positive safe integer`);
67
+ return value;
68
+ }
69
+ /** Require one non-negative safe integer. */
70
+ function nonNegativeInteger(value, field) {
71
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`goal change ${field} must be a non-negative safe integer`);
72
+ return value;
73
+ }
74
+ /** Decode one canonical blocker explanation. */
75
+ function decodeBlockReason(value) {
76
+ if (!isRecord(value) || Object.keys(value).sort().join(",") !== "code,message") throw new Error("goal change goal.blockedReason must have exactly code and message fields");
77
+ if (typeof value["code"] !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value["code"])) throw new Error("goal change goal.blockedReason.code must be lower-kebab-case");
78
+ if (typeof value["message"] !== "string" || value["message"].trim().length === 0 || value["message"] !== value["message"].trim()) throw new Error("goal change goal.blockedReason.message must be non-empty and normalized");
79
+ return {
80
+ code: value["code"],
81
+ message: value["message"]
82
+ };
83
+ }
84
+ /** Decode and validate one snapshot. */
85
+ function decodeSnapshot(value) {
86
+ if (!isRecord(value)) throw new Error("goal change goal must be a record");
87
+ if (typeof value["id"] !== "string" || value["id"].length === 0) throw new Error("goal change goal.id must be a non-empty string");
88
+ if (typeof value["objective"] !== "string" || value["objective"].trim().length === 0 || value["objective"] !== value["objective"].trim()) throw new Error("goal change goal.objective must be non-empty and normalized");
89
+ if (typeof value["phase"] !== "string" || !PHASES.has(value["phase"])) throw new Error("goal change goal.phase is invalid");
90
+ const phase = value["phase"];
91
+ const expectedKeys = phase === "blocked" ? "blockedReason,id,maxGoalRounds,objective,phase,revision" : "id,maxGoalRounds,objective,phase,revision";
92
+ if (Object.keys(value).sort().join(",") !== expectedKeys) throw new Error(`goal change goal for phase ${phase} must have exactly ${expectedKeys} fields`);
93
+ return {
94
+ id: GoalId(value["id"]),
95
+ revision: positiveInteger(value["revision"], "goal.revision"),
96
+ objective: value["objective"],
97
+ phase,
98
+ maxGoalRounds: positiveInteger(value["maxGoalRounds"], "goal.maxGoalRounds"),
99
+ ...phase === "blocked" ? { blockedReason: decodeBlockReason(value["blockedReason"]) } : {}
100
+ };
101
+ }
102
+ /** Decode and validate one ref. */
103
+ function decodeRef(value) {
104
+ if (!isRecord(value) || Object.keys(value).sort().join(",") !== "id,revision") throw new Error("goal clear tombstone must have exactly id and revision fields");
105
+ if (typeof value["id"] !== "string" || value["id"].length === 0) throw new Error("goal clear tombstone id must be a non-empty string");
106
+ return {
107
+ id: GoalId(value["id"]),
108
+ revision: positiveInteger(value["revision"], "cleared.revision")
109
+ };
110
+ }
111
+ /**
112
+ * Decode a value that declares itself as a goal change. Unrelated values
113
+ * return `undefined`; malformed goal changes fail replay loudly.
114
+ * @param value - candidate source change.
115
+ * @returns validated goal change or `undefined` for another value kind.
116
+ */
117
+ function decodeGoalChange(value) {
118
+ if (!isRecord(value) || value["kind"] !== "goal/change") return void 0;
119
+ if (value["version"] !== 1) throw new Error(`unsupported goal change version ${String(value["version"])}`);
120
+ if (value["operation"] === "clear") {
121
+ const allowed = [
122
+ "cleared",
123
+ "clearedAt",
124
+ "kind",
125
+ "operation",
126
+ "version"
127
+ ];
128
+ if (Object.keys(value).sort().join(",") !== allowed.sort().join(",")) throw new Error(`goal clear change must have exactly ${allowed.sort().join(",")} fields`);
129
+ return {
130
+ kind: "goal/change",
131
+ version: 1,
132
+ operation: "clear",
133
+ cleared: decodeRef(value["cleared"]),
134
+ clearedAt: nonNegativeInteger(value["clearedAt"], "clearedAt")
135
+ };
136
+ }
137
+ if (typeof value["operation"] !== "string" || !SNAPSHOT_OPERATIONS.has(value["operation"])) throw new Error("goal change operation is invalid");
138
+ const allowed = [
139
+ "createdAt",
140
+ "goal",
141
+ "kind",
142
+ "operation",
143
+ "roundsStarted",
144
+ "updatedAt",
145
+ "version"
146
+ ];
147
+ if (Object.keys(value).sort().join(",") !== allowed.sort().join(",")) throw new Error(`goal snapshot change must have exactly ${allowed.sort().join(",")} fields`);
148
+ const createdAt = nonNegativeInteger(value["createdAt"], "createdAt");
149
+ const updatedAt = nonNegativeInteger(value["updatedAt"], "updatedAt");
150
+ if (updatedAt < createdAt) throw new Error("goal change updatedAt cannot precede createdAt");
151
+ return {
152
+ kind: "goal/change",
153
+ version: 1,
154
+ operation: value["operation"],
155
+ goal: decodeSnapshot(value["goal"]),
156
+ roundsStarted: nonNegativeInteger(value["roundsStarted"], "roundsStarted"),
157
+ createdAt,
158
+ updatedAt
159
+ };
160
+ }
161
+ /** Narrow model attribution to a valid goal source. */
162
+ function goalSource(source) {
163
+ if (source.kind !== "goal") return void 0;
164
+ if (typeof source.goalId !== "string" || source.goalId.length === 0 || !Number.isSafeInteger(source.revision) || source.revision < 1 || !Number.isSafeInteger(source.round) || source.round < 1) throw new Error("goal message source is invalid");
165
+ return source;
166
+ }
167
+ /** Require two snapshots to retain fields that only `edit` may replace. */
168
+ function requireSameDefinition(current, next, operation) {
169
+ if (next.objective !== current.objective || next.maxGoalRounds !== current.maxGoalRounds) throw new Error(`goal ${operation} cannot change objective or maxGoalRounds`);
170
+ }
171
+ /** Require one exact next revision of the current goal. */
172
+ function requireNextRevision(current, next, operation) {
173
+ if (next.id !== current.id || next.revision !== current.revision + 1) throw new Error(`goal ${operation} must advance the current goal by one revision`);
174
+ }
175
+ /** Validate one non-create snapshot operation against the preceding projection. */
176
+ function validateSnapshotTransition(state, change, current) {
177
+ const next = change.goal;
178
+ requireNextRevision(current, next, change.operation);
179
+ /* v8 ignore next -- a current goal established by this fold always has an updatedAt */
180
+ if (state.updatedAt === void 0) throw new Error("current goal fold lacks updatedAt");
181
+ if (change.createdAt !== state.createdAt || change.updatedAt < state.updatedAt || change.roundsStarted !== state.roundsStarted) throw new Error(`goal ${change.operation} does not preserve the current counters and timestamps`);
182
+ switch (change.operation) {
183
+ case "edit":
184
+ if (next.phase !== current.phase || JSON.stringify(next.blockedReason) !== JSON.stringify(current.blockedReason)) throw new Error("goal edit cannot change phase or blocked reason");
185
+ break;
186
+ case "pause":
187
+ requireSameDefinition(current, next, change.operation);
188
+ if (current.phase !== "active" || next.phase !== "paused") throw new Error("goal pause has an invalid phase transition");
189
+ break;
190
+ case "resume":
191
+ requireSameDefinition(current, next, change.operation);
192
+ if (!new Set([
193
+ "active",
194
+ "paused",
195
+ "blocked"
196
+ ]).has(current.phase) || next.phase !== "active" || state.roundsStarted >= next.maxGoalRounds) throw new Error("goal resume has an invalid phase transition or exhausted round budget");
197
+ break;
198
+ case "complete":
199
+ requireSameDefinition(current, next, change.operation);
200
+ if (current.phase === "complete" || next.phase !== "complete") throw new Error("goal complete has an invalid phase transition");
201
+ break;
202
+ case "block":
203
+ requireSameDefinition(current, next, change.operation);
204
+ if (current.phase !== "active" || next.phase !== "blocked") throw new Error("goal block has an invalid phase transition");
205
+ break;
206
+ /* v8 ignore start -- the caller excludes create and GoalOperation is closed; these arms retain fail-loud exhaustiveness */
207
+ case "create": throw new Error("goal create cannot be validated as a current-goal transition");
208
+ default:
209
+ change.operation;
210
+ throw new Error("unknown goal snapshot operation");
211
+ }
212
+ }
213
+ /**
214
+ * Return the revision identity carried by a snapshot or tombstone.
215
+ * @param change - decoded goal mutation.
216
+ * @returns stable identity used to reconcile a deferred change with its log event.
217
+ */
218
+ function goalChangeRef(change) {
219
+ return change.operation === "clear" ? change.cleared : {
220
+ id: change.goal.id,
221
+ revision: change.goal.revision
222
+ };
223
+ }
224
+ /**
225
+ * Validate and apply one decoded change to a mutable accumulator.
226
+ * @param state - preceding durable goal projection.
227
+ * @param change - decoded full snapshot or clear tombstone.
228
+ */
229
+ function applyGoalChange(state, change) {
230
+ const ref = goalChangeRef(change);
231
+ if (change.operation === "clear") {
232
+ const current = state.goal;
233
+ if (current === void 0) throw new Error("goal clear requires a current goal");
234
+ requireNextRevision(current, change.cleared, change.operation);
235
+ /* v8 ignore next -- a current goal established by this fold always has an updatedAt */
236
+ if (state.updatedAt === void 0) throw new Error("current goal fold lacks updatedAt");
237
+ if (change.clearedAt < state.updatedAt) throw new Error("goal clear timestamp cannot precede the current goal update");
238
+ state.goal = void 0;
239
+ state.roundsStarted = 0;
240
+ state.createdAt = void 0;
241
+ state.updatedAt = void 0;
242
+ state.lastRef = ref;
243
+ return;
244
+ }
245
+ if (change.operation === "create") {
246
+ if (change.goal.revision !== 1 || change.goal.phase !== "active" || change.roundsStarted !== 0 || state.goal !== void 0 && state.goal.phase !== "complete" || state.seenGoalIds.has(change.goal.id)) throw new Error("goal create requires a fresh active revision-one goal with zero rounds");
247
+ state.seenGoalIds.add(change.goal.id);
248
+ } else {
249
+ const current = state.goal;
250
+ if (current === void 0) throw new Error(`goal ${change.operation} requires a current goal`);
251
+ validateSnapshotTransition(state, change, current);
252
+ }
253
+ state.goal = change.goal;
254
+ state.roundsStarted = change.roundsStarted;
255
+ state.createdAt = change.createdAt;
256
+ state.updatedAt = change.updatedAt;
257
+ state.lastRef = ref;
258
+ }
259
+ /**
260
+ * Apply one session event to the strict durable goal fold.
261
+ * @param state - mutable fold accumulator.
262
+ * @param event - next event in sequence order.
263
+ */
264
+ function applyGoalEvent(state, event) {
265
+ if (event.type === "goal/change") {
266
+ const change = decodeGoalChange(event.data);
267
+ /* v8 ignore next -- the event's declared payload always identifies itself as a goal change. */
268
+ if (change === void 0) throw new Error(`goal change at session event ${event.seq} has an invalid kind`);
269
+ applyGoalChange(state, change);
270
+ return;
271
+ }
272
+ if (event.type === "user/message") {
273
+ const source = goalSource(event.data.source);
274
+ if (source === void 0) return;
275
+ const current = state.goal;
276
+ if (current === void 0 || current.phase !== "active" || source.goalId !== current.id || source.revision !== current.revision || source.round !== state.roundsStarted + 1 || source.round > current.maxGoalRounds) throw new Error(`goal round at session event ${event.seq} is not the next admitted round of the active goal`);
277
+ state.roundsStarted = source.round;
278
+ }
279
+ }
280
+ /**
281
+ * Fold current goal state from a contiguous session event log.
282
+ * @param events - session events in sequence order.
283
+ * @returns a fresh durable projection; activation is deliberately absent.
284
+ */
285
+ function foldGoal(events) {
286
+ const state = emptyGoalFoldState();
287
+ for (const event of events) applyGoalEvent(state, event);
288
+ return {
289
+ ...state.goal === void 0 ? {} : { goal: { ...state.goal } },
290
+ roundsStarted: state.roundsStarted,
291
+ ...state.createdAt === void 0 ? {} : { createdAt: state.createdAt },
292
+ ...state.updatedAt === void 0 ? {} : { updatedAt: state.updatedAt },
293
+ ...state.lastRef === void 0 ? {} : { lastRef: { ...state.lastRef } }
294
+ };
295
+ }
296
+ //#endregion
297
+ //#region lib/types/index.js
298
+ /**
299
+ * Same-session goal domain: event-sourced state, compare-and-set mutations,
300
+ * and process-local continuation activation.
301
+ * @module @stackstackstack/dsh-goal
302
+ */
303
+ var __runInitializers = function(thisArg, initializers, value) {
304
+ var useValue = arguments.length > 2;
305
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
306
+ return useValue ? value : void 0;
307
+ };
308
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
309
+ function accept(f) {
310
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
311
+ return f;
312
+ }
313
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
314
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
315
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
316
+ var _, done = false;
317
+ for (var i = decorators.length - 1; i >= 0; i--) {
318
+ var context = {};
319
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
320
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
321
+ context.addInitializer = function(f) {
322
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
323
+ extraInitializers.push(accept(f || null));
324
+ };
325
+ var result = (0, decorators[i])(kind === "accessor" ? {
326
+ get: descriptor.get,
327
+ set: descriptor.set
328
+ } : descriptor[key], context);
329
+ if (kind === "accessor") {
330
+ if (result === void 0) continue;
331
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
332
+ if (_ = accept(result.get)) descriptor.get = _;
333
+ if (_ = accept(result.set)) descriptor.set = _;
334
+ if (_ = accept(result.init)) initializers.unshift(_);
335
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
336
+ else descriptor[key] = _;
337
+ }
338
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
339
+ done = true;
340
+ };
341
+ /** Wire payload schema of the `goal` projection (whole current goal or pre-create/cleared null). */
342
+ const goalProjectionSchema = z$1.union([z$1.object({
343
+ goal: z$1.object({
344
+ id: z$1.string().min(1),
345
+ revision: z$1.number().int().positive(),
346
+ objective: z$1.string().min(1),
347
+ phase: z$1.union([
348
+ z$1.literal("active"),
349
+ z$1.literal("paused"),
350
+ z$1.literal("blocked"),
351
+ z$1.literal("complete")
352
+ ]),
353
+ blockedReason: z$1.object({
354
+ code: z$1.string(),
355
+ message: z$1.string()
356
+ }).optional(),
357
+ maxGoalRounds: z$1.number().int().positive()
358
+ }),
359
+ roundsStarted: z$1.number().int().nonnegative(),
360
+ createdAt: z$1.number(),
361
+ updatedAt: z$1.number()
362
+ }), z$1.null()]);
363
+ /**
364
+ * Light last-wins fold of the `goal` projection unit. Unlike the strict
365
+ * replay fold (fold.ts: transition validation, fail-loud on malformed
366
+ * changes, Set-typed state), this transition is projection-grade: the state
367
+ * is plain JSON (persisted-cache precondition), any non-goal or malformed
368
+ * event returns the same reference (the registry's Object.is gate — the
369
+ * title/todos posture), and correctness of the written change is the write
370
+ * side's job (GoalService validated it before appending; the package
371
+ * invariant rejects a violating stream fail-loud where it is installed).
372
+ * @param state - the projection covering all prior events.
373
+ * @param event - the next committed session event.
374
+ * @returns the next projection (same reference when the event is not a goal change).
375
+ */
376
+ function applyGoalProjection(state, event) {
377
+ if (event.type !== "goal/change") return state;
378
+ let change;
379
+ try {
380
+ change = decodeGoalChange(event.data);
381
+ } catch (_invalidPersistedGoalChange) {
382
+ return state;
383
+ }
384
+ if (change === void 0) return state;
385
+ return change.operation === "clear" ? null : {
386
+ goal: change.goal,
387
+ roundsStarted: change.roundsStarted,
388
+ createdAt: change.createdAt,
389
+ updatedAt: change.updatedAt
390
+ };
391
+ }
392
+ /** Validate a caller-visible positive safe-integer round cap. */
393
+ function resolveMaxGoalRounds(value) {
394
+ if (!Number.isSafeInteger(value) || value < 1) throw new GoalError("maxGoalRounds must be a positive safe integer", "GOAL_INVALID_MAX_ROUNDS");
395
+ return value;
396
+ }
397
+ /** Validate and normalize an objective at the domain boundary. */
398
+ function resolveObjective(value) {
399
+ if (typeof value !== "string" || value.trim().length === 0) throw new GoalError("goal objective must be a non-empty string", "GOAL_INVALID_OBJECTIVE");
400
+ return value.trim();
401
+ }
402
+ /** Materialize deployment defaults and validate one create request. */
403
+ function resolveCreateGoal(request, defaultMaxGoalRounds) {
404
+ return {
405
+ objective: resolveObjective(request.objective),
406
+ maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds ?? defaultMaxGoalRounds)
407
+ };
408
+ }
409
+ /** Validate and detach one policy-owned blocker explanation. */
410
+ function resolveBlockReason(reason) {
411
+ const record = typeof reason === "object" && reason !== null && !Array.isArray(reason) ? reason : void 0;
412
+ const code = record?.["code"];
413
+ const message = record?.["message"];
414
+ if (typeof code !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(code) || typeof message !== "string" || message.trim().length === 0) throw new GoalError("goal block reason requires a lower-kebab-case code and a non-empty message", "GOAL_INVALID_BLOCK_REASON");
415
+ return {
416
+ code,
417
+ message: message.trim()
418
+ };
419
+ }
420
+ /** Goal service (`ctx.goals`) backed exclusively by the owning session log. */
421
+ let GoalService = (() => {
422
+ let _classSuper = TypertRemoteService;
423
+ let _instanceExtraInitializers = [];
424
+ let _edit_decorators;
425
+ let _pause_decorators;
426
+ let _resume_decorators;
427
+ let _complete_decorators;
428
+ let _clear_decorators;
429
+ let _remoteExportCreate_decorators;
430
+ return class GoalService extends _classSuper {
431
+ static {
432
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
433
+ _edit_decorators = [Remote("edit")];
434
+ _pause_decorators = [Remote("pause")];
435
+ _resume_decorators = [Remote("resume")];
436
+ _complete_decorators = [Remote("complete")];
437
+ _clear_decorators = [Remote("clear")];
438
+ _remoteExportCreate_decorators = [Remote("create")];
439
+ __esDecorate(this, null, _edit_decorators, {
440
+ kind: "method",
441
+ name: "edit",
442
+ static: false,
443
+ private: false,
444
+ access: {
445
+ has: (obj) => "edit" in obj,
446
+ get: (obj) => obj.edit
447
+ },
448
+ metadata: _metadata
449
+ }, null, _instanceExtraInitializers);
450
+ __esDecorate(this, null, _pause_decorators, {
451
+ kind: "method",
452
+ name: "pause",
453
+ static: false,
454
+ private: false,
455
+ access: {
456
+ has: (obj) => "pause" in obj,
457
+ get: (obj) => obj.pause
458
+ },
459
+ metadata: _metadata
460
+ }, null, _instanceExtraInitializers);
461
+ __esDecorate(this, null, _resume_decorators, {
462
+ kind: "method",
463
+ name: "resume",
464
+ static: false,
465
+ private: false,
466
+ access: {
467
+ has: (obj) => "resume" in obj,
468
+ get: (obj) => obj.resume
469
+ },
470
+ metadata: _metadata
471
+ }, null, _instanceExtraInitializers);
472
+ __esDecorate(this, null, _complete_decorators, {
473
+ kind: "method",
474
+ name: "complete",
475
+ static: false,
476
+ private: false,
477
+ access: {
478
+ has: (obj) => "complete" in obj,
479
+ get: (obj) => obj.complete
480
+ },
481
+ metadata: _metadata
482
+ }, null, _instanceExtraInitializers);
483
+ __esDecorate(this, null, _clear_decorators, {
484
+ kind: "method",
485
+ name: "clear",
486
+ static: false,
487
+ private: false,
488
+ access: {
489
+ has: (obj) => "clear" in obj,
490
+ get: (obj) => obj.clear
491
+ },
492
+ metadata: _metadata
493
+ }, null, _instanceExtraInitializers);
494
+ __esDecorate(this, null, _remoteExportCreate_decorators, {
495
+ kind: "method",
496
+ name: "remoteExportCreate",
497
+ static: false,
498
+ private: false,
499
+ access: {
500
+ has: (obj) => "remoteExportCreate" in obj,
501
+ get: (obj) => obj.remoteExportCreate
502
+ },
503
+ metadata: _metadata
504
+ }, null, _instanceExtraInitializers);
505
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
506
+ enumerable: true,
507
+ configurable: true,
508
+ writable: true,
509
+ value: _metadata
510
+ });
511
+ }
512
+ static inject = ["agents"];
513
+ static Config = z.object({ defaultMaxGoalRounds: z.number().default(256) });
514
+ resolved = __runInitializers(this, _instanceExtraInitializers);
515
+ caches = /* @__PURE__ */ new WeakMap();
516
+ constructor(ctx, config = {}) {
517
+ super(ctx, "goals");
518
+ this.resolved = { defaultMaxGoalRounds: resolveMaxGoalRounds(config.defaultMaxGoalRounds ?? 256) };
519
+ ctx.on("agent/session-start", ({ agent }) => {
520
+ this.cache(agent.session).activation = "disarmed";
521
+ });
522
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
523
+ projectionCtx.sessionProjections.register({
524
+ key: "goal",
525
+ schema: goalProjectionSchema,
526
+ init: () => null,
527
+ apply: applyGoalProjection,
528
+ view: (state) => state,
529
+ stateVersion: 4
530
+ });
531
+ });
532
+ }
533
+ /**
534
+ * Read the current goal for one exact live agent.
535
+ * @param agent - owning live agent.
536
+ * @returns a fresh view or `undefined` when no goal is current.
537
+ * @throws {@link GoalError} when the agent is not the registry's live instance.
538
+ */
539
+ get(agent) {
540
+ this.assertLive(agent);
541
+ const cache = this.cache(agent.session);
542
+ this.sync(agent.session, cache);
543
+ return this.view(cache);
544
+ }
545
+ /**
546
+ * Remove process-local continuation authority without changing durable goal
547
+ * phase or revision. Lifecycle owners use this before unloading a driver;
548
+ * a later human-authorized {@link resume} records the new activation edge.
549
+ * @param agent - owning live agent.
550
+ * @returns a fresh disarmed view, or `undefined` when no goal is current.
551
+ */
552
+ disarm(agent) {
553
+ this.assertLive(agent);
554
+ const cache = this.cache(agent.session);
555
+ this.sync(agent.session, cache);
556
+ cache.activation = "disarmed";
557
+ return this.view(cache);
558
+ }
559
+ /**
560
+ * Create and arm a goal. A completed goal may be replaced; every other
561
+ * current phase must be cleared or resumed instead.
562
+ * @param agent - owning live agent.
563
+ * @param request - objective and optional round cap.
564
+ * @returns the created live view.
565
+ */
566
+ create(agent, request) {
567
+ const spec = resolveCreateGoal(request, this.resolved.defaultMaxGoalRounds);
568
+ const cache = this.prepareMutation(agent);
569
+ const current = cache.state.goal;
570
+ if (current !== void 0 && current.phase !== "complete") throw new GoalError(`goal "${current.id}" already exists with phase "${current.phase}"`, "GOAL_ALREADY_EXISTS");
571
+ const now = Date.now();
572
+ const goal = {
573
+ id: GoalId(`goal-${randomUUID()}`),
574
+ revision: 1,
575
+ objective: spec.objective,
576
+ phase: "active",
577
+ maxGoalRounds: spec.maxGoalRounds
578
+ };
579
+ return this.commitSnapshot(agent, cache, "create", goal, 0, now, now, "armed");
580
+ }
581
+ /**
582
+ * Edit objective and/or round cap without changing phase.
583
+ * @param agent - owning live agent.
584
+ * @param ref - expected current revision.
585
+ * @param request - at least one replacement field.
586
+ * @returns the edited view.
587
+ */
588
+ edit(agent, ref, request) {
589
+ const cache = this.prepareMutation(agent);
590
+ const current = this.expectCurrent(cache, ref);
591
+ if (request.objective === void 0 && request.maxGoalRounds === void 0) throw new GoalError("goal edit requires objective and/or maxGoalRounds", "GOAL_INVALID_EDIT");
592
+ const goal = {
593
+ ...current,
594
+ revision: current.revision + 1,
595
+ ...request.objective === void 0 ? {} : { objective: resolveObjective(request.objective) },
596
+ ...request.maxGoalRounds === void 0 ? {} : { maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds) }
597
+ };
598
+ return this.commitCurrent(agent, cache, "edit", goal, cache.activation);
599
+ }
600
+ /**
601
+ * Pause an active goal and disarm automatic continuation.
602
+ * @param agent - owning live agent.
603
+ * @param ref - expected current revision.
604
+ * @returns the paused view.
605
+ */
606
+ pause(agent, ref) {
607
+ return this.transition(agent, ref, "pause", ["active"], "paused", "disarmed");
608
+ }
609
+ /**
610
+ * Resume and arm a stopped goal, or rearm an active goal after a
611
+ * session-start edge, while its round budget still has capacity.
612
+ * @param agent - owning live agent.
613
+ * @param ref - expected current revision.
614
+ * @returns the active view.
615
+ */
616
+ resume(agent, ref) {
617
+ const cache = this.prepareMutation(agent);
618
+ const current = this.expectCurrent(cache, ref);
619
+ const resumable = [
620
+ "active",
621
+ "paused",
622
+ "blocked"
623
+ ];
624
+ if (!resumable.includes(current.phase)) throw this.transitionError(current, "resume", resumable);
625
+ if (current.phase === "active" && cache.activation === "armed") throw new GoalError(`goal "${current.id}" is already active and armed`, "GOAL_INVALID_TRANSITION");
626
+ if (cache.state.roundsStarted >= current.maxGoalRounds) throw new GoalError(`goal "${current.id}" exhausted ${current.maxGoalRounds} goal rounds; increase maxGoalRounds before resuming`, "GOAL_INVALID_TRANSITION");
627
+ return this.commitCurrent(agent, cache, "resume", this.withPhase(current, "active"), "armed");
628
+ }
629
+ /**
630
+ * Mark a current non-complete goal complete and disarm it.
631
+ * @param agent - owning live agent.
632
+ * @param ref - expected current revision.
633
+ * @returns the completed view.
634
+ */
635
+ complete(agent, ref) {
636
+ return this.transition(agent, ref, "complete", [
637
+ "active",
638
+ "paused",
639
+ "blocked"
640
+ ], "complete", "disarmed");
641
+ }
642
+ /**
643
+ * Mark an active goal blocked and disarm it.
644
+ * @param agent - owning live agent.
645
+ * @param ref - expected current revision.
646
+ * @param reason - policy-owned stable code and human-readable explanation.
647
+ * @returns the blocked view with its durable reason.
648
+ */
649
+ block(agent, ref, reason) {
650
+ const cache = this.prepareMutation(agent);
651
+ const current = this.expectCurrent(cache, ref);
652
+ if (current.phase !== "active") throw this.transitionError(current, "block", ["active"]);
653
+ return this.commitCurrent(agent, cache, "block", {
654
+ ...this.withPhase(current, "blocked"),
655
+ blockedReason: resolveBlockReason(reason)
656
+ }, "disarmed");
657
+ }
658
+ /**
659
+ * Clear the current goal while retaining a durable tombstone and history.
660
+ * @param agent - owning live agent.
661
+ * @param ref - expected current revision.
662
+ * @returns the tombstone ref whose revision is one past the cleared snapshot.
663
+ */
664
+ clear(agent, ref) {
665
+ const cache = this.prepareMutation(agent);
666
+ const current = this.expectCurrent(cache, ref);
667
+ const tombstone = {
668
+ id: current.id,
669
+ revision: current.revision + 1
670
+ };
671
+ const change = {
672
+ kind: "goal/change",
673
+ version: 1,
674
+ operation: "clear",
675
+ cleared: tombstone,
676
+ clearedAt: this.nextMutationTime(cache)
677
+ };
678
+ this.commit(agent, cache, change, "disarmed");
679
+ return { ...tombstone };
680
+ }
681
+ /** Resolve and validate the cache used by a mutation. */
682
+ prepareMutation(agent) {
683
+ this.assertLive(agent);
684
+ const cache = this.cache(agent.session);
685
+ this.sync(agent.session, cache);
686
+ return cache;
687
+ }
688
+ /** Reject stale or missing current-state refs. */
689
+ expectCurrent(cache, ref) {
690
+ const current = cache.state.goal;
691
+ if (current === void 0) throw new GoalError("no current goal", "GOAL_NOT_FOUND");
692
+ if (ref.id !== current.id || ref.revision !== current.revision) throw new GoalError(`stale goal ref "${ref.id}" revision ${ref.revision}; current is "${current.id}" revision ${current.revision}`, "GOAL_STALE_REVISION");
693
+ return current;
694
+ }
695
+ /** Enforce exact live-agent identity rather than trusting a matching id. */
696
+ assertLive(agent) {
697
+ if (this.ctx.agents.get(agent.id) !== agent) throw new GoalError(`agent "${agent.id}" is not live in this registry`, "GOAL_AGENT_NOT_LIVE");
698
+ }
699
+ /** Return the per-session cache, folding a seed once with activation disarmed. */
700
+ cache(session) {
701
+ let cache = this.caches.get(session);
702
+ if (cache !== void 0) return cache;
703
+ const state = emptyGoalFoldState();
704
+ for (const event of session.events) applyGoalEvent(state, event);
705
+ cache = {
706
+ state,
707
+ activation: "disarmed",
708
+ observedSeq: session.seq,
709
+ pendingActivation: void 0
710
+ };
711
+ this.caches.set(session, cache);
712
+ return cache;
713
+ }
714
+ /** Incrementally observe durable events and reconcile local activation intent. */
715
+ sync(session, cache) {
716
+ for (const event of session.events.slice(cache.observedSeq)) {
717
+ applyGoalEvent(cache.state, event);
718
+ if (event.type === "goal/change") cache.activation = cache.pendingActivation?.seq === event.seq ? cache.pendingActivation.activation : "disarmed";
719
+ cache.observedSeq += 1;
720
+ }
721
+ }
722
+ /** Build a new revision with one replacement phase. */
723
+ withPhase(current, phase) {
724
+ return {
725
+ id: current.id,
726
+ revision: current.revision + 1,
727
+ objective: current.objective,
728
+ phase,
729
+ maxGoalRounds: current.maxGoalRounds
730
+ };
731
+ }
732
+ /** Shared validated phase transition. */
733
+ transition(agent, ref, operation, allowed, phase, activation) {
734
+ const cache = this.prepareMutation(agent);
735
+ const current = this.expectCurrent(cache, ref);
736
+ if (!allowed.includes(current.phase)) throw this.transitionError(current, operation, allowed);
737
+ return this.commitCurrent(agent, cache, operation, this.withPhase(current, phase), activation);
738
+ }
739
+ /** Render a stable invalid-transition error. */
740
+ transitionError(current, operation, allowed) {
741
+ return new GoalError(`cannot ${operation} goal "${current.id}" from phase "${current.phase}"; expected ${allowed.join(" or ")}`, "GOAL_INVALID_TRANSITION");
742
+ }
743
+ /** Commit a mutation that retains the current goal's derived counters/times. */
744
+ commitCurrent(agent, cache, operation, goal, activation) {
745
+ const createdAt = cache.state.createdAt;
746
+ /* v8 ignore next -- strict replay and every snapshot commit set createdAt whenever a current goal exists */
747
+ if (createdAt === void 0) throw new Error("current goal cache lacks createdAt");
748
+ return this.commitSnapshot(agent, cache, operation, goal, cache.state.roundsStarted, createdAt, this.nextMutationTime(cache), activation);
749
+ }
750
+ /** Clamp a current goal's next timestamp across backward wall-clock movement. */
751
+ nextMutationTime(cache) {
752
+ const updatedAt = cache.state.updatedAt;
753
+ /* v8 ignore next -- strict replay and every snapshot commit set updatedAt whenever a current goal exists */
754
+ if (updatedAt === void 0) throw new Error("current goal cache lacks updatedAt");
755
+ return Math.max(Date.now(), updatedAt);
756
+ }
757
+ /** Build and commit one full-snapshot mutation. */
758
+ commitSnapshot(agent, cache, operation, goal, roundsStarted, createdAt, updatedAt, activation) {
759
+ const change = {
760
+ kind: "goal/change",
761
+ version: 1,
762
+ operation,
763
+ goal,
764
+ roundsStarted,
765
+ createdAt,
766
+ updatedAt
767
+ };
768
+ this.commit(agent, cache, change, activation);
769
+ const view = this.view(cache);
770
+ /* v8 ignore next -- the durable goal event installs the snapshot before this read */
771
+ if (view === void 0) throw new Error("snapshot commit cleared the goal unexpectedly");
772
+ return view;
773
+ }
774
+ /** Commit one mutation into the goal log, cache, and live event stream. */
775
+ commit(agent, cache, change, activation) {
776
+ const ref = goalChangeRef(change);
777
+ cache.pendingActivation = {
778
+ seq: agent.session.seq,
779
+ activation
780
+ };
781
+ try {
782
+ agent.session.append("goal/change", change);
783
+ this.sync(agent.session, cache);
784
+ } finally {
785
+ cache.pendingActivation = void 0;
786
+ }
787
+ const goal = this.view(cache);
788
+ const notification = {
789
+ operation: change.operation,
790
+ ref: { ...ref },
791
+ ...goal === void 0 ? {} : { goal }
792
+ };
793
+ agentEvents(this.ctx, agent).emit("goal/changed", { change: notification });
794
+ }
795
+ /** Build a detached current view. */
796
+ view(cache) {
797
+ const goal = cache.state.goal;
798
+ const createdAt = cache.state.createdAt;
799
+ const updatedAt = cache.state.updatedAt;
800
+ if (goal === void 0) return void 0;
801
+ /* v8 ignore next 3 -- strict replay and snapshot commits establish both timestamps with every current goal */
802
+ if (createdAt === void 0 || updatedAt === void 0) throw new Error(`goal "${goal.id}" cache lacks timestamps`);
803
+ return {
804
+ ...goal,
805
+ roundsStarted: cache.state.roundsStarted,
806
+ createdAt,
807
+ updatedAt,
808
+ activation: cache.activation
809
+ };
810
+ }
811
+ /**
812
+ * Create one Goal through the remote boundary.
813
+ * @param agent - exact live Agent resolved from the wire identity.
814
+ * @param request - objective and optional round cap.
815
+ * @returns the created Goal identity.
816
+ */
817
+ remoteExportCreate(agent, request) {
818
+ const view = this.create(agent, request);
819
+ return { ref: {
820
+ id: view.id,
821
+ revision: view.revision
822
+ } };
823
+ }
824
+ };
825
+ })();
826
+ //#endregion
827
+ export { GOAL_CHANGE_VERSION, GoalError, GoalId, GoalService, GoalService as default, applyGoalProjection, decodeGoalChange, foldGoal, goalChangeRef };