opencode-goal-plugin 0.8.0 → 0.8.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.2 — 2026-08-21
6
+
7
+ - Harden the post-compaction goal continuation guard introduced in
8
+ [#58](https://github.com/willytop8/OpenCode-goal-plugin/pull/58). Building on
9
+ the epoch-guard groundwork contributed by
10
+ [@harryzhou2000](https://github.com/harryzhou2000), the stalled-compaction
11
+ circuit breaker no longer misfires on two conditions that OpenCode produces in
12
+ practice:
13
+ - A re-delivered `session.compacted` event no longer counts as a second
14
+ compaction. Real OpenCode compaction events carry only a `sessionID` (no
15
+ identity field), so identity-based deduplication could never fire against a
16
+ live host; a duplicate host delivery could therefore trip the two-strike
17
+ breaker and abort the session. Re-deliveries are now recognized by the
18
+ absence of any message activity since the previous compaction.
19
+ - A `[goal:complete]` or `[goal:blocked]` reported on the assistant turn that
20
+ a compaction retains as its continuation source is now honored instead of
21
+ being suppressed, so a finished goal is archived rather than driven for one
22
+ more redundant continuation.
23
+ - Remove a productive-turn detection branch that inspected message parts on the
24
+ `message.updated` event. That event never carries parts (they arrive on
25
+ `message.part.updated`, which the plugin does not observe), so the check was
26
+ inert; the stalled-compaction breaker resets on assistant output-token
27
+ progress, which a tool-using turn also produces.
28
+
29
+ ## 0.8.1 — 2026-08-07
30
+
31
+ - Fix goal auto-continue stalling after session compaction: a continuation
32
+ claim for a pre-compaction source turn could match the still-visible tail
33
+ assistant message after compaction and suppress the post-compaction
34
+ continuation until the user nudged the goal. The claim is now invalidated on
35
+ `session.compacted`, so the loop resumes on the next idle. Contributed by
36
+ [@harryzhou2000](https://github.com/harryzhou2000) in
37
+ [#58](https://github.com/willytop8/OpenCode-goal-plugin/pull/58).
38
+ - Update the bundled `zod` dependency from 4.1.8 to 4.4.3.
39
+
5
40
  ## 0.8.0 — 2026-08-06
6
41
 
7
42
  Both new options in this release were contributed by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Durable, guarded goal workflows for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./src/goal-plugin.js",
@@ -72,9 +72,9 @@
72
72
  "name": "willytop8"
73
73
  },
74
74
  "devDependencies": {
75
- "typescript": "5.9.3"
75
+ "typescript": "7.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "zod": "4.1.8"
78
+ "zod": "4.4.3"
79
79
  }
80
80
  }
@@ -40,6 +40,7 @@ function legacyHomeStateFilePath(env = process.env) {
40
40
  return join(homeBase(env), ".opencode-goal-plugin", "state.json")
41
41
  }
42
42
  const MAX_HISTORY_ENTRIES = 20
43
+ const MAX_STALLED_COMPACTIONS = 2
43
44
  // Marks a plugin-synthesized parent wake so the receiving pass knows it is
44
45
  // re-examining an assistant turn that has already been scored.
45
46
  const CHILD_WAKE_EVENT_FLAG = Symbol.for("opencode-goal-plugin.childWake")
@@ -1104,6 +1105,11 @@ function resetGoalBudget(goal) {
1104
1105
  goal.formatFailures = 0
1105
1106
  goal.lastAssistantMessageID = ""
1106
1107
  goal.continuationClaim = null
1108
+ goal.compactionEpoch = 0
1109
+ goal.stalledCompactions = 0
1110
+ goal.lastCompactionEventID = ""
1111
+ goal.messageSeenSinceCompaction = true
1112
+ goal.compactionSourceAssistantMessageID = ""
1107
1113
  goal.skipNextTerminalCheck = false
1108
1114
  goal.history = [...(goal.history || [])].slice(-MAX_HISTORY_ENTRIES)
1109
1115
  }
@@ -1433,15 +1439,31 @@ function normalizePersistedGoal(rawGoal) {
1433
1439
  stopReason: typeof rawGoal.stopReason === "string" ? rawGoal.stopReason : "",
1434
1440
  promptFailures: toNonNegativeInteger(rawGoal.promptFailures),
1435
1441
  formatFailures: toNonNegativeInteger(rawGoal.formatFailures),
1442
+ compactionEpoch: toNonNegativeInteger(rawGoal.compactionEpoch),
1443
+ stalledCompactions: toNonNegativeInteger(rawGoal.stalledCompactions),
1444
+ lastCompactionEventID:
1445
+ typeof rawGoal.lastCompactionEventID === "string" &&
1446
+ rawGoal.lastCompactionEventID.length <= MAX_GOAL_META_LENGTH
1447
+ ? rawGoal.lastCompactionEventID
1448
+ : "",
1449
+ messageSeenSinceCompaction: rawGoal.messageSeenSinceCompaction !== false,
1450
+ compactionSourceAssistantMessageID:
1451
+ typeof rawGoal.compactionSourceAssistantMessageID === "string" &&
1452
+ rawGoal.compactionSourceAssistantMessageID.length <= MAX_GOAL_META_LENGTH
1453
+ ? rawGoal.compactionSourceAssistantMessageID
1454
+ : "",
1436
1455
  executionContext: normalizeExecutionContext(rawGoal.executionContext),
1437
1456
  continuationClaim:
1438
1457
  isPlainObject(rawGoal.continuationClaim) &&
1439
1458
  typeof rawGoal.continuationClaim.runId === "string" &&
1440
1459
  rawGoal.continuationClaim.runId.length <= MAX_GOAL_META_LENGTH &&
1460
+ Number.isSafeInteger(rawGoal.continuationClaim.compactionEpoch) &&
1461
+ rawGoal.continuationClaim.compactionEpoch >= 0 &&
1441
1462
  typeof rawGoal.continuationClaim.sourceAssistantMessageID === "string" &&
1442
1463
  rawGoal.continuationClaim.sourceAssistantMessageID.length <= MAX_GOAL_META_LENGTH
1443
1464
  ? {
1444
1465
  runId: rawGoal.continuationClaim.runId,
1466
+ compactionEpoch: rawGoal.continuationClaim.compactionEpoch,
1445
1467
  sourceAssistantMessageID: rawGoal.continuationClaim.sourceAssistantMessageID,
1446
1468
  }
1447
1469
  : null,
@@ -2635,7 +2657,42 @@ function systemBlockContainsGoal(block, goalId) {
2635
2657
  }
2636
2658
 
2637
2659
  function findLatestAssistantMessage(messages) {
2638
- return [...(messages || [])].reverse().find((message) => messageRole(message) === "assistant") || null
2660
+ return [...(messages || [])]
2661
+ .reverse()
2662
+ .find(
2663
+ (message) =>
2664
+ messageRole(message) === "assistant" && !isCompactionAssistantMessage(message),
2665
+ ) || null
2666
+ }
2667
+
2668
+ function isCompactionAssistantMessage(message) {
2669
+ if (messageRole(message) !== "assistant") return false
2670
+ const info = isPlainObject(message?.info) ? message.info : message
2671
+ return (
2672
+ info?.summary === true ||
2673
+ info?.agent === "compaction" ||
2674
+ info?.mode === "compaction" ||
2675
+ message?.agent === "compaction" ||
2676
+ message?.mode === "compaction"
2677
+ )
2678
+ }
2679
+
2680
+ function compactionEventIdentity(event) {
2681
+ const candidates = [
2682
+ event?.id,
2683
+ event?.properties?.compactionID,
2684
+ event?.properties?.summaryID,
2685
+ event?.properties?.messageID,
2686
+ event?.properties?.id,
2687
+ event?.data?.compactionID,
2688
+ event?.data?.summaryID,
2689
+ event?.data?.messageID,
2690
+ event?.data?.id,
2691
+ ]
2692
+ const identity = candidates.find(
2693
+ (candidate) => typeof candidate === "string" && candidate.length > 0,
2694
+ )
2695
+ return identity && identity.length <= MAX_GOAL_META_LENGTH ? identity : ""
2639
2696
  }
2640
2697
 
2641
2698
  function messageParentID(message) {
@@ -2771,6 +2828,7 @@ function continuationSnapshot(messages, ownedMessages = currentRuntime().ownedPl
2771
2828
  .reverse()
2772
2829
  .find((message) =>
2773
2830
  (messageRole(message) === "assistant" || messageRole(message) === "user") &&
2831
+ !isCompactionAssistantMessage(message) &&
2774
2832
  !isPluginGeneratedMessage(message, ownedMessages),
2775
2833
  )
2776
2834
  return {
@@ -2939,6 +2997,11 @@ function buildGoalState(sessionID, condition, options, meta = {}, lastStatus = "
2939
2997
  stopReason: "",
2940
2998
  promptFailures: 0,
2941
2999
  formatFailures: 0,
3000
+ compactionEpoch: 0,
3001
+ stalledCompactions: 0,
3002
+ lastCompactionEventID: "",
3003
+ messageSeenSinceCompaction: true,
3004
+ compactionSourceAssistantMessageID: "",
2942
3005
  executionContext: normalizeExecutionContext(
2943
3006
  meta.executionContext || currentRuntime().sessionExecutionContexts.get(sessionID),
2944
3007
  ),
@@ -4397,18 +4460,19 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4397
4460
  sessionID,
4398
4461
  goalID,
4399
4462
  runID,
4463
+ compactionEpoch,
4400
4464
  baselineMessages,
4401
4465
  { refreshMessages = false } = {},
4402
4466
  ) => {
4403
4467
  const goalBeforeRefresh = activeGoal(sessionID, goalID, runID)
4404
- if (!goalBeforeRefresh) return null
4468
+ if (!goalBeforeRefresh || goalBeforeRefresh.compactionEpoch !== compactionEpoch) return null
4405
4469
  const hostMessages = refreshMessages
4406
4470
  ? await sessionApi.messages(sessionID, {
4407
4471
  limit: goalBeforeRefresh.options.maxRecentMessages,
4408
4472
  })
4409
4473
  : baselineMessages
4410
4474
  const goal = activeGoal(sessionID, goalID, runID)
4411
- if (!goal) return null
4475
+ if (!goal || goal.compactionEpoch !== compactionEpoch) return null
4412
4476
  const messages = Array.isArray(hostMessages)
4413
4477
  ? hostMessages.slice(-goal.options.maxRecentMessages)
4414
4478
  : []
@@ -4523,12 +4587,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4523
4587
  const sourceAssistantMessageID = refreshed.latestAssistantID || "<no-assistant>"
4524
4588
  if (
4525
4589
  goal.continuationClaim?.runId === runID &&
4590
+ goal.continuationClaim?.compactionEpoch === compactionEpoch &&
4526
4591
  goal.continuationClaim?.sourceAssistantMessageID === sourceAssistantMessageID
4527
4592
  ) {
4528
4593
  return null
4529
4594
  }
4530
4595
 
4531
- goal.continuationClaim = { runId: runID, sourceAssistantMessageID }
4596
+ goal.continuationClaim = { runId: runID, compactionEpoch, sourceAssistantMessageID }
4532
4597
  const claimPersisted = await persist(sessionID)
4533
4598
  if (!claimPersisted && persistenceOptions.persistState) {
4534
4599
  goal.continuationClaim = null
@@ -4545,7 +4610,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4545
4610
  })
4546
4611
  return null
4547
4612
  }
4548
- return goal
4613
+ // Let an already-published compaction event invalidate this claim before
4614
+ // the caller enters promptAsync. The final epoch check is the atomic edge:
4615
+ // a claim is valid only while its context epoch is still current.
4616
+ await Promise.resolve()
4617
+ return activeGoal(sessionID, goalID, runID)?.compactionEpoch === compactionEpoch
4618
+ ? goal
4619
+ : null
4549
4620
  }
4550
4621
 
4551
4622
  const retireCompletedCommandTurnOnIdle = async (sessionID, messageLimit) => {
@@ -5328,9 +5399,59 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5328
5399
  if (event?.type === "session.compacted") {
5329
5400
  const sessionID = getSessionID(event)
5330
5401
  const goal = goalStates.get(sessionID)
5331
- if (!goal) return
5402
+ if (!goal || goal.stopped) return
5403
+ const identity = compactionEventIdentity(event)
5404
+ if (identity) {
5405
+ if (identity === goal.lastCompactionEventID) return
5406
+ goal.lastCompactionEventID = identity
5407
+ } else if (goal.compactionEpoch > 0 && !goal.messageSeenSinceCompaction) {
5408
+ // A real OpenCode `session.compacted` carries only `sessionID` (SDK:
5409
+ // EventSessionCompacted has no id/compactionID/summaryID/messageID and
5410
+ // no sync variant), so compactionEventIdentity() returns "" for every
5411
+ // host-delivered compaction and the identity dedup above never fires
5412
+ // in production. Recognize a re-delivery by the absence of message
5413
+ // activity instead: a genuine new compaction is always preceded by
5414
+ // messages, because the context has to grow again to trigger one.
5415
+ return
5416
+ }
5417
+ goal.messageSeenSinceCompaction = false
5418
+
5419
+ goal.compactionEpoch += 1
5420
+ goal.stalledCompactions += 1
5421
+ goal.compactionSourceAssistantMessageID =
5422
+ goal.continuationClaim?.runId === goal.runId
5423
+ ? goal.continuationClaim.sourceAssistantMessageID
5424
+ : ""
5332
5425
  goal.messageIDs = new Set()
5333
5426
  goal.totalTokens = 0
5427
+ // Compaction rewrites the context. The epoch-scoped claim lets the same
5428
+ // retained assistant source continue once in the new epoch without
5429
+ // allowing duplicate idle delivery to continue it twice.
5430
+ goal.continuationClaim = null
5431
+
5432
+ // An idle handler can already have persisted its source claim when the
5433
+ // compaction lands. Abort its cooldown and release the per-session guard;
5434
+ // the epoch checks around promptAsync prevent that stale handler from
5435
+ // sending while allowing the post-compaction idle to start immediately.
5436
+ currentRuntime().continuationControllers.get(sessionID)?.abort()
5437
+ currentRuntime().continuationControllers.delete(sessionID)
5438
+ activeContinues.delete(sessionID)
5439
+
5440
+ if (goal.stalledCompactions >= MAX_STALLED_COMPACTIONS) {
5441
+ await pauseActiveGoal(sessionID, {
5442
+ stopReason: "stalled compaction",
5443
+ status: `Goal paused after ${goal.stalledCompactions} compactions without a productive assistant or tool turn.`,
5444
+ history: `Paused after ${goal.stalledCompactions} compactions without productive non-compaction work.`,
5445
+ })
5446
+ if (typeof client?.session?.abort === "function") {
5447
+ try {
5448
+ await sessionApi.abort(sessionID)
5449
+ } catch (error) {
5450
+ await logPluginError(client, "Failed to abort a stalled compaction loop", error)
5451
+ }
5452
+ }
5453
+ return
5454
+ }
5334
5455
  await persist(sessionID)
5335
5456
  return
5336
5457
  }
@@ -5338,6 +5459,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5338
5459
  if (event?.type === "message.updated") {
5339
5460
  const message = messageInfoFromEvent(event)
5340
5461
  if (!message) return
5462
+ const messageEnvelope =
5463
+ event?.properties?.message || event?.data?.message || message
5341
5464
 
5342
5465
  const currentMessageID = messageID(message)
5343
5466
  if (!currentMessageID) return
@@ -5347,6 +5470,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5347
5470
  const goal = goalStates.get(currentSessionID)
5348
5471
  if (!goal) return
5349
5472
 
5473
+ // Any message traffic for this goal marks the current compaction epoch
5474
+ // as having seen activity, which is what lets an identity-less
5475
+ // `session.compacted` re-delivery be told apart from a real one. Recorded
5476
+ // before the stale-redelivery guard below: a message that is stale for
5477
+ // token accounting still proves the host is delivering message events.
5478
+ goal.messageSeenSinceCompaction = true
5479
+
5350
5480
  // Skip stale re-deliveries from a prior budget window or a replaced goal.
5351
5481
  // resetGoalBudget and cleanupGoal both leave seenTokens entries in place
5352
5482
  // so this guard can fire: if an ID is already recorded in seenTokens but
@@ -5389,6 +5519,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5389
5519
 
5390
5520
  if (
5391
5521
  messageRole(message) === "assistant" &&
5522
+ !isCompactionAssistantMessage(messageEnvelope) &&
5523
+ currentMessageID !== goal.compactionSourceAssistantMessageID &&
5392
5524
  currentOutputTokens > previousOutputTokens &&
5393
5525
  runtime.suppressedCommandAssistants.get(currentMessageID) !== currentSessionID
5394
5526
  ) {
@@ -5396,6 +5528,25 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5396
5528
  changed = true
5397
5529
  }
5398
5530
 
5531
+ // Productive-turn reset. `message.updated` carries only
5532
+ // `properties.info` (SDK: EventMessageUpdated) and never any parts —
5533
+ // tool parts arrive on the separate `message.part.updated` event, which
5534
+ // this plugin does not observe. A messageHasToolCall() check against the
5535
+ // event envelope is therefore always false and cannot serve as the reset
5536
+ // signal. Growing output tokens is the signal that does work: an
5537
+ // assistant turn that calls a tool still emits output tokens for it.
5538
+ if (
5539
+ messageRole(message) === "assistant" &&
5540
+ !isCompactionAssistantMessage(messageEnvelope) &&
5541
+ currentMessageID !== goal.compactionSourceAssistantMessageID &&
5542
+ currentOutputTokens > previousOutputTokens &&
5543
+ goal.stalledCompactions > 0
5544
+ ) {
5545
+ goal.stalledCompactions = 0
5546
+ goal.compactionSourceAssistantMessageID = ""
5547
+ changed = true
5548
+ }
5549
+
5399
5550
  if (changed) await persist(messageSessionID(message))
5400
5551
  return
5401
5552
  }
@@ -5491,10 +5642,12 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5491
5642
  if (!goal || goal.stopped || activeContinues.has(sessionID)) return
5492
5643
  const goalID = goal.goalId
5493
5644
  const runID = goal.runId
5645
+ const compactionEpoch = goal.compactionEpoch
5494
5646
 
5495
5647
  const continueToken = randomUUID()
5496
5648
  const continueController = new AbortController()
5497
5649
  let claimedSourceAssistantMessageID = ""
5650
+ let claimedCompactionEpoch = -1
5498
5651
  activeContinues.set(sessionID, continueToken)
5499
5652
  currentRuntime().continuationControllers.set(sessionID, continueController)
5500
5653
  try {
@@ -5507,7 +5660,10 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5507
5660
  ? hostMessages.slice(-goal.options.maxRecentMessages)
5508
5661
  : []
5509
5662
  const activeGoalAfterMessages = activeGoal(sessionID, goalID, runID)
5510
- if (!activeGoalAfterMessages) return
5663
+ if (
5664
+ !activeGoalAfterMessages ||
5665
+ activeGoalAfterMessages.compactionEpoch !== compactionEpoch
5666
+ ) return
5511
5667
  if (!activeGoalAfterMessages.executionContext) {
5512
5668
  activeGoalAfterMessages.executionContext = findLatestExecutionContext(messages)
5513
5669
  }
@@ -5520,9 +5676,21 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5520
5676
  const assistantChanged = summarizeText(latestText) !== summarizeText(previousAssistantText)
5521
5677
  const assistantRepeated =
5522
5678
  latestAssistantID && latestAssistantID === activeGoalAfterMessages.lastAssistantMessageID
5523
- const activationBoundary =
5679
+ // A retained pre-compaction assistant must not be scored as fresh
5680
+ // progress, so the compaction source gates checkpointing and the stall
5681
+ // heuristics. It must NOT gate the terminal checks: a [goal:complete] or
5682
+ // [goal:blocked] on that retained turn has not been acted on yet — it
5683
+ // survived the compaction unprocessed — and swallowing it discards a
5684
+ // real result and spends another continuation to re-derive it.
5685
+ const terminalBoundary =
5524
5686
  currentRuntime().suppressedCommandAssistants.get(latestAssistantID) === sessionID ||
5525
5687
  activeGoalAfterMessages.skipNextTerminalCheck === true
5688
+ const activationBoundary =
5689
+ terminalBoundary ||
5690
+ Boolean(
5691
+ activeGoalAfterMessages.compactionSourceAssistantMessageID &&
5692
+ activeGoalAfterMessages.compactionSourceAssistantMessageID === latestAssistantID,
5693
+ )
5526
5694
  activeGoalAfterMessages.skipNextTerminalCheck = false
5527
5695
 
5528
5696
  if (!activationBoundary && latestText && (!assistantRepeated || assistantChanged)) {
@@ -5549,6 +5717,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5549
5717
  const sourceAssistantMessageID = latestAssistantID || "<no-assistant>"
5550
5718
  if (
5551
5719
  activeGoalAfterMessages.continuationClaim?.runId === runID &&
5720
+ activeGoalAfterMessages.continuationClaim?.compactionEpoch === compactionEpoch &&
5552
5721
  activeGoalAfterMessages.continuationClaim?.sourceAssistantMessageID ===
5553
5722
  sourceAssistantMessageID
5554
5723
  ) {
@@ -5563,7 +5732,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5563
5732
  let completionUnverified = false
5564
5733
  let blockerUnstated = false
5565
5734
 
5566
- if (!activationBoundary && goalIsComplete(latestText)) {
5735
+ if (!terminalBoundary && goalIsComplete(latestText)) {
5567
5736
  const evidence = extractCompletionEvidence(latestText)
5568
5737
  if (evidence) {
5569
5738
  await announceAudit(
@@ -5725,7 +5894,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5725
5894
  "completion-unverified",
5726
5895
  "Assistant output [goal:complete] without a [goal:evidence] line; completion rejected, continuing.",
5727
5896
  )
5728
- } else if (!activationBoundary && goalIsBlocked(latestText)) {
5897
+ } else if (!terminalBoundary && goalIsBlocked(latestText)) {
5729
5898
  const reason = extractBlockedReason(latestText)
5730
5899
  if (reason) {
5731
5900
  await announceAudit(
@@ -5795,6 +5964,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5795
5964
  sessionID,
5796
5965
  goalID,
5797
5966
  runID,
5967
+ compactionEpoch,
5798
5968
  messages,
5799
5969
  )
5800
5970
  if (!claimedGoal) return
@@ -6008,12 +6178,16 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6008
6178
  sessionID,
6009
6179
  goalID,
6010
6180
  runID,
6181
+ compactionEpoch,
6011
6182
  messages,
6012
6183
  { refreshMessages: cooldownWaited },
6013
6184
  )
6014
6185
  if (!activeGoalBeforePrompt) return
6015
6186
  claimedSourceAssistantMessageID =
6016
6187
  activeGoalBeforePrompt.continuationClaim?.sourceAssistantMessageID || ""
6188
+ claimedCompactionEpoch =
6189
+ activeGoalBeforePrompt.continuationClaim?.compactionEpoch ?? -1
6190
+ if (claimedCompactionEpoch !== activeGoalBeforePrompt.compactionEpoch) return
6017
6191
 
6018
6192
  const budgetWrapup = budgetWrapupNeeded(activeGoalBeforePrompt)
6019
6193
  if (budgetWrapup) {
@@ -6109,6 +6283,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6109
6283
  const activeGoalAfterPrompt = currentGoal(sessionID, goalID, runID)
6110
6284
  const message = `Auto-continue failed: ${response.error.name || "unknown error"}`
6111
6285
  if (
6286
+ activeGoalAfterPrompt?.continuationClaim?.compactionEpoch ===
6287
+ claimedCompactionEpoch &&
6112
6288
  activeGoalAfterPrompt?.continuationClaim?.sourceAssistantMessageID ===
6113
6289
  claimedSourceAssistantMessageID
6114
6290
  ) {
@@ -6127,6 +6303,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6127
6303
  } else {
6128
6304
  const activeGoalAfterPrompt = currentGoal(sessionID, goalID, runID)
6129
6305
  if (
6306
+ activeGoalAfterPrompt?.continuationClaim?.compactionEpoch ===
6307
+ claimedCompactionEpoch &&
6130
6308
  activeGoalAfterPrompt?.continuationClaim?.sourceAssistantMessageID ===
6131
6309
  claimedSourceAssistantMessageID
6132
6310
  ) {
@@ -6158,6 +6336,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6158
6336
  if (activeGoalAfterError) {
6159
6337
  if (
6160
6338
  claimedSourceAssistantMessageID &&
6339
+ activeGoalAfterError.continuationClaim?.compactionEpoch ===
6340
+ claimedCompactionEpoch &&
6161
6341
  activeGoalAfterError.continuationClaim?.sourceAssistantMessageID ===
6162
6342
  claimedSourceAssistantMessageID
6163
6343
  ) {