From 3c2e07a92d48c712f707603e78bab4954e03a33f Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Mon, 20 Jul 2026 08:38:28 +0800
Subject: [PATCH] fix(goal): make publication reentrancy-safe
---
...rsisted-same-session-goal-domain.i18n.yaml | 4 +-
...7-19-persisted-same-session-goal-domain.md | 5 +-
...9-persisted-same-session-goal-domain.zh.md | 5 +-
docs/cordis-catalog/services.md | 2 +-
docs/glossary.md | 2 +-
packages/goal/goal/README.md | 3 +-
packages/goal/goal/src/index.ts | 47 +++++++---
packages/goal/goal/tests/goal.spec.ts | 92 +++++++++++++++++++
website/zh-CN/api/harness/goals.md | 24 ++---
9 files changed, 152 insertions(+), 32 deletions(-)
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
index 86c1ae0715..d398150b73 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-19-persisted-same-session-goal-domain.md: 3404e24c3cc5880f0eeddbdbf35e01ad32063b74
-2026-07-19-persisted-same-session-goal-domain.zh.md: fb12aad5d7c035a2befc30ffd650dcd336233104
+2026-07-19-persisted-same-session-goal-domain.md: 1484a919851c18ce978c93c068f6096bbf3b733f
+2026-07-19-persisted-same-session-goal-domain.zh.md: 8bcedc6cab6b9219a33a655e8fcbc53762b7413f
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
index 3404e24c3c..1484a91985 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
@@ -22,7 +22,7 @@ Every non-clear mutation uses `Agent.inject()` to append a raw, model-visible `c
The replay fold validates JSON shape, source attribution, rendered content, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds are positive sequential `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
-When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; the durable log remains authoritative after restart.
+When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; reentrant append observers project each mutation exactly once. Incremental replay advances its cursor after each valid event and remains positioned at the first corrupt event, so later reads report the same durable fault. The durable log remains authoritative after restart.
### Lifecycle and live activation
@@ -36,7 +36,7 @@ The service accepts only the exact live `Agent` object registered under its id.
## Testing
-Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, cap enforcement, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start disarming and active-goal rearming, FIFO deferred mutation reconciliation, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
+Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, cap enforcement, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start disarming and active-goal rearming, FIFO deferred mutation reconciliation, reentrant append observation, rejected-injection rollback, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
## Alternatives considered
@@ -59,4 +59,5 @@ Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set r
- This domain records state but does not schedule goal rounds, cancel active turns, or classify abnormal stops.
- The actor that records `complete` or `blocked` is authoritative; an independent evaluator or completion certificate is deferred to a policy consumer.
- There is one current goal per session; parallel objective graphs and cross-session goal storage are absent.
+- Plugins share one trusted process boundary. Direct session writers can counterfeit goal records; strict replay detects inconsistency and fails goal access at the offending record, but does not isolate plugins or repair the log.
- `GOAL_CHANGE_VERSION` has no pre-release compatibility promise or migration path.
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
index fb12aad5d7..8bcedc6cab 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
@@ -22,7 +22,7 @@ Status: implemented
回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
-当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重启后仍以持久日志为准。
+当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
### 生命周期与实时激活态
@@ -36,7 +36,7 @@ Status: implemented
## 测试
-单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
+单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
## 考虑过的替代方案
@@ -59,4 +59,5 @@ Status: implemented
- 本领域记录状态,但不调度目标回合、不取消活跃轮次,也不分类异常停止。
- 记录 `complete` 或 `blocked` 的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。
- 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
+- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
- `GOAL_CHANGE_VERSION` 在首次发布前不承诺兼容性,也不提供迁移路径。
diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md
index 934086dd97..7d33e49c13 100644
--- a/docs/cordis-catalog/services.md
+++ b/docs/cordis-catalog/services.md
@@ -577,7 +577,7 @@ clear(agent: Agent, ref: GoalRef): GoalRef
Types: [Agent](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [CreateGoalSpec](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/goal.md)
-Source: [`packages/goal/goal/src/index.ts:97`](../../packages/goal/goal/src/index.ts)
+Source: [`packages/goal/goal/src/index.ts:104`](../../packages/goal/goal/src/index.ts)
## `ctx.llm` — `LlmService`
diff --git a/docs/glossary.md b/docs/glossary.md
index 480ef461ff..0166f01f63 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -26,4 +26,4 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
- **turn** — one drain of admitted input in a session, ending after the model and its tools stop or a terminal policy intervenes.
- **step** — one model request plus the tool executions caused by its response; a turn contains one or more steps.
-- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round) or one fresh-agent Ralph attempt. Round counters belong to that policy and do not count every turn in a session.
+- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round). Round counters belong to that policy and do not count every turn in a session.
diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md
index 4247ccb5e6..0e0807f684 100644
--- a/packages/goal/goal/README.md
+++ b/packages/goal/goal/README.md
@@ -21,7 +21,7 @@ At most one goal is current. Creation produces an active revision-one goal and a
Every non-clear mutation appends a complete versioned snapshot through `agent.inject()`; clear appends a revisioned tombstone. The raw `context/message`, its `{ kind: 'goal' }` source, and its metadata must agree exactly. Replay rejects malformed shapes, source/content drift, discontinuous revisions, illegal lifecycle transitions, non-monotonic per-goal timestamps, and non-sequential goal rounds. Mutation timestamps clamp against the preceding goal update when wall time moves backward.
-Injection may append immediately or wait in an active tool-batch FIFO. The service overlays accepted pending changes in memory and reconciles each exact payload when it enters the log, so consecutive model-tool mutations see their own latest revisions without treating an unlogged cache as durable state. `goal/changed` fires after the append or enqueue succeeds; listener failures are contained.
+Injection may append immediately or wait in an active tool-batch FIFO. The service overlays accepted pending changes in memory and reconciles each exact payload when it enters the log, so consecutive model-tool mutations see their own latest revisions without treating an unlogged cache as durable state. Reentrant append observers see each accepted mutation exactly once, and incremental replay retains its cursor at the first corrupt event. `goal/changed` fires after the append or enqueue succeeds; listener failures are contained.
Activation is never persisted. A fresh cache and every `agent/session-start` edge disarm it even when replay finds an active durable phase. Session resume and fork therefore retain the objective, phase, revisions, and admitted-round count without initiating work; a later explicit resume mutation must arm continuation.
@@ -51,3 +51,4 @@ Append-only within an epoch: each mutation follows the reusable request prefix a
- **Round-count budget only** — `maxGoalRounds` does not meter tokens, currency, wall time, or provider quotas.
- **No independent evaluator** — the caller that records completion or blocking is authoritative; evaluator-backed certification is deferred to a separate policy layer.
- **One current goal** — parallel objectives and a separate goal database are intentionally absent; history remains available in the session log after replacement or clear.
+- **Trusted in-process producers** — a plugin with direct `Session` access can append counterfeit goal metadata. Strict replay detects malformed or inconsistent records and leaves goal access failed at that record until the log is repaired; this is integrity detection, not plugin isolation.
diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts
index 726ceb6c53..c8583894ef 100644
--- a/packages/goal/goal/src/index.ts
+++ b/packages/goal/goal/src/index.ts
@@ -64,12 +64,19 @@ export interface ResolvedConfig {
defaultMaxGoalRounds: number
}
+/** One accepted mutation waiting to enter or be observed in the session log. */
+interface PendingGoalChange {
+ readonly change: GoalChangeMeta
+ readonly activation: GoalActivation
+ applied: boolean
+}
+
/** Process-local cache plus mutations waiting in the active tool-batch FIFO. */
interface GoalCache {
readonly state: GoalFoldState
activation: GoalActivation
observedSeq: number
- readonly pending: GoalChangeMeta[]
+ readonly pending: PendingGoalChange[]
}
/** Validate a caller-visible positive safe-integer round cap. */
@@ -358,15 +365,21 @@ export class GoalService extends Service {
const change = decodeGoalEvent(event)
if (change !== undefined) {
const pending = cache.pending[0]
- if (pending !== undefined && sameChange(pending, change)) {
+ if (pending !== undefined && sameChange(pending.change, change)) {
+ if (!pending.applied) {
+ applyGoalChange(cache.state, change)
+ cache.activation = pending.activation
+ pending.applied = true
+ }
cache.pending.shift()
+ cache.observedSeq += 1
continue
}
}
}
applyGoalEvent(cache.state, event)
+ cache.observedSeq += 1
}
- cache.observedSeq = session.seq
}
/** Build a new revision with one replacement phase. */
@@ -464,14 +477,26 @@ export class GoalService extends Service {
const meta = snapshotJsonValue(change) as JsonValue | undefined
/* v8 ignore next -- validated goal changes contain only finite JSON primitives and records */
if (meta === undefined) throw new Error('goal change is not losslessly JSON-serializable')
- agent.inject(renderGoalChange(change), {
- source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0 },
- envelope: 'raw',
- meta,
- })
- cache.pending.push(change)
- applyGoalChange(cache.state, change)
- cache.activation = activation
+ const pending: PendingGoalChange = { change, activation, applied: false }
+ cache.pending.push(pending)
+ try {
+ agent.inject(renderGoalChange(change), {
+ source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0 },
+ envelope: 'raw',
+ meta,
+ })
+ } catch (error: unknown) {
+ const index = cache.pending.indexOf(pending)
+ /* v8 ignore next -- a committed goal append cannot reject after its contained observers run */
+ if (index < 0) throw new Error('goal injection failed after its pending mutation was reconciled', { cause: error })
+ cache.pending.splice(index, 1)
+ throw error
+ }
+ if (!pending.applied) {
+ applyGoalChange(cache.state, change)
+ cache.activation = activation
+ pending.applied = true
+ }
this.sync(agent.session, cache)
const goal = this.view(cache)
const notification: GoalChanged = {
diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts
index c8aa5bc3e3..4dcae8938a 100644
--- a/packages/goal/goal/tests/goal.spec.ts
+++ b/packages/goal/goal/tests/goal.spec.ts
@@ -235,6 +235,25 @@ describe('GoalService creation and replay', () => {
expect(() => foldGoal(session.events)).not.toThrow()
})
+ it('removes the service and its session-start listener with the providing fiber', async () => {
+ const ctx = new Context()
+ await ctx.plugin(AgentRegistry)
+ const fiber = await ctx.plugin(GoalService)
+ const first = ctx.goals
+ const stub = stubAgent('goal-hmr')
+ ctx.agents.register(stub.agent)
+ const goal = first.create(stub.agent, { objective: 'survive service reload' })
+
+ await fiber.dispose()
+ expect(ctx.get('goals')).toBeUndefined()
+ agentEvents(ctx, stub.agent).emit('agent/session-start', 'resume')
+ expect(first.get(stub.agent)).toMatchObject({ id: goal.id, activation: 'armed' })
+
+ await ctx.plugin(GoalService)
+ expect(ctx.goals).not.toBe(first)
+ expect(ctx.goals.get(stub.agent)).toMatchObject({ id: goal.id, activation: 'disarmed' })
+ })
+
it('requires the exact live registry instance for reads and mutations', async () => {
const { ctx, agent } = await harness()
const impostor = { ...agent, session: new Session(agent.id) }
@@ -404,6 +423,46 @@ describe('GoalService mutations', () => {
expect(foldGoal(session.events)).toMatchObject({ goal: { revision: 3, phase: 'paused' } })
})
+ it('publishes a mutation consistently to a reentrant session observer', async () => {
+ const ctx = new Context()
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(GoalService)
+ const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')))
+ ctx.agents.register(stub.agent)
+ let observed: ReturnType
+ ctx.on('session/event', (session, event) => {
+ if (session === stub.session && event.type === 'context/message') observed = ctx.goals.get(stub.agent)
+ })
+
+ const created = ctx.goals.create(stub.agent, { objective: 'publish once' })
+
+ expect(observed).toEqual(created)
+ expect(ctx.goals.get(stub.agent)).toEqual(created)
+ expect(foldGoal(stub.session.events)).toMatchObject({ goal: { id: created.id, revision: 1 } })
+ })
+
+ it('rolls back a pending mutation when injection rejects before append', async () => {
+ const ctx = new Context()
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(GoalService)
+ const stub = stubAgent('goal-rejected-injection')
+ const append = stub.agent.inject.bind(stub.agent)
+ let reject = true
+ stub.agent.inject = (content, options) => {
+ if (reject) throw new Error('injection rejected')
+ append(content, options)
+ }
+ ctx.agents.register(stub.agent)
+
+ expect(() => ctx.goals.create(stub.agent, { objective: 'first attempt' })).toThrow('injection rejected')
+ reject = false
+ expect(ctx.goals.create(stub.agent, { objective: 'second attempt' })).toMatchObject({
+ objective: 'second attempt',
+ revision: 1,
+ })
+ })
+
it('rejects deferred goal mutations that enter the log out of FIFO order', async () => {
const test = await harness()
test.setDeferred(true)
@@ -447,6 +506,39 @@ describe('GoalService mutations', () => {
activation: 'disarmed',
})
})
+
+ it('reports the same corrupt unseen event after committing its valid prefix', async () => {
+ const { ctx, agent, session } = await harness()
+ expect(ctx.goals.get(agent)).toBeUndefined()
+ const change: GoalSnapshotChangeMeta = {
+ kind: 'goal/change',
+ version: 1,
+ operation: 'create',
+ goal: {
+ id: GoalId('goal-valid-prefix'),
+ revision: 1,
+ objective: 'valid prefix',
+ phase: 'active',
+ maxGoalRounds: 4,
+ },
+ roundsStarted: 0,
+ createdAt: 12,
+ updatedAt: 12,
+ }
+ appendInjection(session, renderGoalChange(change), {
+ source: { kind: 'goal', goalId: change.goal.id, revision: 1, round: 0 },
+ envelope: 'raw',
+ meta: change as never,
+ })
+ appendInjection(session, [{ type: 'text', text: 'corrupt' }], {
+ source: { kind: 'goal', goalId: change.goal.id, revision: 2, round: 0 },
+ envelope: 'raw',
+ meta: { ...change, operation: 'edit', extra: true } as never,
+ })
+
+ expect(() => ctx.goals.get(agent)).toThrow('invalid shape')
+ expect(() => ctx.goals.get(agent)).toThrow('invalid shape')
+ })
})
describe('goal replay validation', () => {
diff --git a/website/zh-CN/api/harness/goals.md b/website/zh-CN/api/harness/goals.md
index 146cb04193..8789074e36 100644
--- a/website/zh-CN/api/harness/goals.md
+++ b/website/zh-CN/api/harness/goals.md
@@ -6,7 +6,7 @@
Goal service (`ctx.goals`) backed exclusively by the owning session log.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L97)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L104)
### ctx.goals.resolveCreate(request)
@@ -25,7 +25,7 @@ Materialize deployment defaults and validate one create request.
**Returns** detached, fully resolved create specification.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L122)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L129)
### ctx.goals.get(agent)
@@ -45,7 +45,7 @@ Read the current goal for one exact live agent.
**Returns** a fresh view or `undefined` when no goal is current.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L135)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L142)
### ctx.goals.create(agent, request)
@@ -67,7 +67,7 @@ Create and arm a goal. A completed goal may be replaced; every other current pha
**Returns** the created live view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L149)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L156)
### ctx.goals.edit(agent, ref, request)
@@ -90,7 +90,7 @@ Edit objective and/or round cap without changing phase.
**Returns** the edited view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L174)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L181)
### ctx.goals.pause(agent, ref)
@@ -111,7 +111,7 @@ Pause an active goal and disarm automatic continuation.
**Returns** the paused view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L195)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L202)
### ctx.goals.resume(agent, ref)
@@ -133,7 +133,7 @@ Resume and arm a stopped goal, or rearm an active goal after a session-start edg
**Returns** the active view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L206)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L213)
### ctx.goals.complete(agent, ref)
@@ -154,7 +154,7 @@ Mark a current non-complete goal complete and disarm it.
**Returns** the completed view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L231)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L238)
### ctx.goals.block(agent, ref)
@@ -175,7 +175,7 @@ Mark an active goal blocked and disarm it.
**Returns** the blocked view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L248)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L255)
### ctx.goals.markUsageLimited(agent, ref)
@@ -196,7 +196,7 @@ Mark an active goal stopped by an external usage limit.
**Returns** the usage-limited view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L258)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L265)
### ctx.goals.markBudgetLimited(agent, ref)
@@ -217,7 +217,7 @@ Mark an active goal stopped at its configured round cap.
**Returns** the budget-limited view.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L268)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L275)
### ctx.goals.clear(agent, ref)
@@ -238,4 +238,4 @@ Clear the current goal while retaining a durable tombstone and history.
**Returns** the tombstone ref whose revision is one past the cleared snapshot.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L295)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L302)