opencode-goal-plugin 0.8.1 → 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,30 @@
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
+
5
29
  ## 0.8.1 — 2026-08-07
6
30
 
7
31
  - Fix goal auto-continue stalling after session compaction: a continuation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-plugin",
3
- "version": "0.8.1",
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",
@@ -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,15 +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
5334
- // Compaction rewrites the context: a continuation claim for a
5335
- // pre-compaction source turn must not suppress the post-compaction
5336
- // continuation (the recent tail can still end on the same assistant
5337
- // message, which would otherwise stall the goal loop until the user
5338
- // nudges it).
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.
5339
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
+ }
5340
5455
  await persist(sessionID)
5341
5456
  return
5342
5457
  }
@@ -5344,6 +5459,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5344
5459
  if (event?.type === "message.updated") {
5345
5460
  const message = messageInfoFromEvent(event)
5346
5461
  if (!message) return
5462
+ const messageEnvelope =
5463
+ event?.properties?.message || event?.data?.message || message
5347
5464
 
5348
5465
  const currentMessageID = messageID(message)
5349
5466
  if (!currentMessageID) return
@@ -5353,6 +5470,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5353
5470
  const goal = goalStates.get(currentSessionID)
5354
5471
  if (!goal) return
5355
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
+
5356
5480
  // Skip stale re-deliveries from a prior budget window or a replaced goal.
5357
5481
  // resetGoalBudget and cleanupGoal both leave seenTokens entries in place
5358
5482
  // so this guard can fire: if an ID is already recorded in seenTokens but
@@ -5395,6 +5519,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5395
5519
 
5396
5520
  if (
5397
5521
  messageRole(message) === "assistant" &&
5522
+ !isCompactionAssistantMessage(messageEnvelope) &&
5523
+ currentMessageID !== goal.compactionSourceAssistantMessageID &&
5398
5524
  currentOutputTokens > previousOutputTokens &&
5399
5525
  runtime.suppressedCommandAssistants.get(currentMessageID) !== currentSessionID
5400
5526
  ) {
@@ -5402,6 +5528,25 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5402
5528
  changed = true
5403
5529
  }
5404
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
+
5405
5550
  if (changed) await persist(messageSessionID(message))
5406
5551
  return
5407
5552
  }
@@ -5497,10 +5642,12 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5497
5642
  if (!goal || goal.stopped || activeContinues.has(sessionID)) return
5498
5643
  const goalID = goal.goalId
5499
5644
  const runID = goal.runId
5645
+ const compactionEpoch = goal.compactionEpoch
5500
5646
 
5501
5647
  const continueToken = randomUUID()
5502
5648
  const continueController = new AbortController()
5503
5649
  let claimedSourceAssistantMessageID = ""
5650
+ let claimedCompactionEpoch = -1
5504
5651
  activeContinues.set(sessionID, continueToken)
5505
5652
  currentRuntime().continuationControllers.set(sessionID, continueController)
5506
5653
  try {
@@ -5513,7 +5660,10 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5513
5660
  ? hostMessages.slice(-goal.options.maxRecentMessages)
5514
5661
  : []
5515
5662
  const activeGoalAfterMessages = activeGoal(sessionID, goalID, runID)
5516
- if (!activeGoalAfterMessages) return
5663
+ if (
5664
+ !activeGoalAfterMessages ||
5665
+ activeGoalAfterMessages.compactionEpoch !== compactionEpoch
5666
+ ) return
5517
5667
  if (!activeGoalAfterMessages.executionContext) {
5518
5668
  activeGoalAfterMessages.executionContext = findLatestExecutionContext(messages)
5519
5669
  }
@@ -5526,9 +5676,21 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5526
5676
  const assistantChanged = summarizeText(latestText) !== summarizeText(previousAssistantText)
5527
5677
  const assistantRepeated =
5528
5678
  latestAssistantID && latestAssistantID === activeGoalAfterMessages.lastAssistantMessageID
5529
- 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 =
5530
5686
  currentRuntime().suppressedCommandAssistants.get(latestAssistantID) === sessionID ||
5531
5687
  activeGoalAfterMessages.skipNextTerminalCheck === true
5688
+ const activationBoundary =
5689
+ terminalBoundary ||
5690
+ Boolean(
5691
+ activeGoalAfterMessages.compactionSourceAssistantMessageID &&
5692
+ activeGoalAfterMessages.compactionSourceAssistantMessageID === latestAssistantID,
5693
+ )
5532
5694
  activeGoalAfterMessages.skipNextTerminalCheck = false
5533
5695
 
5534
5696
  if (!activationBoundary && latestText && (!assistantRepeated || assistantChanged)) {
@@ -5555,6 +5717,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5555
5717
  const sourceAssistantMessageID = latestAssistantID || "<no-assistant>"
5556
5718
  if (
5557
5719
  activeGoalAfterMessages.continuationClaim?.runId === runID &&
5720
+ activeGoalAfterMessages.continuationClaim?.compactionEpoch === compactionEpoch &&
5558
5721
  activeGoalAfterMessages.continuationClaim?.sourceAssistantMessageID ===
5559
5722
  sourceAssistantMessageID
5560
5723
  ) {
@@ -5569,7 +5732,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5569
5732
  let completionUnverified = false
5570
5733
  let blockerUnstated = false
5571
5734
 
5572
- if (!activationBoundary && goalIsComplete(latestText)) {
5735
+ if (!terminalBoundary && goalIsComplete(latestText)) {
5573
5736
  const evidence = extractCompletionEvidence(latestText)
5574
5737
  if (evidence) {
5575
5738
  await announceAudit(
@@ -5731,7 +5894,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5731
5894
  "completion-unverified",
5732
5895
  "Assistant output [goal:complete] without a [goal:evidence] line; completion rejected, continuing.",
5733
5896
  )
5734
- } else if (!activationBoundary && goalIsBlocked(latestText)) {
5897
+ } else if (!terminalBoundary && goalIsBlocked(latestText)) {
5735
5898
  const reason = extractBlockedReason(latestText)
5736
5899
  if (reason) {
5737
5900
  await announceAudit(
@@ -5801,6 +5964,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5801
5964
  sessionID,
5802
5965
  goalID,
5803
5966
  runID,
5967
+ compactionEpoch,
5804
5968
  messages,
5805
5969
  )
5806
5970
  if (!claimedGoal) return
@@ -6014,12 +6178,16 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6014
6178
  sessionID,
6015
6179
  goalID,
6016
6180
  runID,
6181
+ compactionEpoch,
6017
6182
  messages,
6018
6183
  { refreshMessages: cooldownWaited },
6019
6184
  )
6020
6185
  if (!activeGoalBeforePrompt) return
6021
6186
  claimedSourceAssistantMessageID =
6022
6187
  activeGoalBeforePrompt.continuationClaim?.sourceAssistantMessageID || ""
6188
+ claimedCompactionEpoch =
6189
+ activeGoalBeforePrompt.continuationClaim?.compactionEpoch ?? -1
6190
+ if (claimedCompactionEpoch !== activeGoalBeforePrompt.compactionEpoch) return
6023
6191
 
6024
6192
  const budgetWrapup = budgetWrapupNeeded(activeGoalBeforePrompt)
6025
6193
  if (budgetWrapup) {
@@ -6115,6 +6283,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6115
6283
  const activeGoalAfterPrompt = currentGoal(sessionID, goalID, runID)
6116
6284
  const message = `Auto-continue failed: ${response.error.name || "unknown error"}`
6117
6285
  if (
6286
+ activeGoalAfterPrompt?.continuationClaim?.compactionEpoch ===
6287
+ claimedCompactionEpoch &&
6118
6288
  activeGoalAfterPrompt?.continuationClaim?.sourceAssistantMessageID ===
6119
6289
  claimedSourceAssistantMessageID
6120
6290
  ) {
@@ -6133,6 +6303,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6133
6303
  } else {
6134
6304
  const activeGoalAfterPrompt = currentGoal(sessionID, goalID, runID)
6135
6305
  if (
6306
+ activeGoalAfterPrompt?.continuationClaim?.compactionEpoch ===
6307
+ claimedCompactionEpoch &&
6136
6308
  activeGoalAfterPrompt?.continuationClaim?.sourceAssistantMessageID ===
6137
6309
  claimedSourceAssistantMessageID
6138
6310
  ) {
@@ -6164,6 +6336,8 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
6164
6336
  if (activeGoalAfterError) {
6165
6337
  if (
6166
6338
  claimedSourceAssistantMessageID &&
6339
+ activeGoalAfterError.continuationClaim?.compactionEpoch ===
6340
+ claimedCompactionEpoch &&
6167
6341
  activeGoalAfterError.continuationClaim?.sourceAssistantMessageID ===
6168
6342
  claimedSourceAssistantMessageID
6169
6343
  ) {