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 +24 -0
- package/package.json +1 -1
- package/src/goal-plugin.js +189 -15
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
package/src/goal-plugin.js
CHANGED
|
@@ -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 || [])]
|
|
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
|
-
|
|
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
|
|
5335
|
-
//
|
|
5336
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
) {
|