dsh-ptc-plus 0.5.6 → 0.5.7

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.
@@ -8,7 +8,7 @@ import { RUN_CODE } from './runtime-bridge-owner.js'
8
8
  import { sessionExecutionAvailable } from './host-tool-sdk.js'
9
9
 
10
10
  const CORDIS_PRESET_ID = 'cordis'
11
- const CORDIS_SKILL_NAME = 'cordis-plugin-development'
11
+ const COMPANION_SKILL_NAMES = Object.freeze(['dsh-doc', 'cordis-plugin-development'])
12
12
  const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
13
13
  const COMPANION_SKILL_TOOL = 'skill'
14
14
  const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
@@ -22,19 +22,19 @@ const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
22
22
  }),
23
23
  ])
24
24
 
25
- function exactCompanionCandidates(observation) {
25
+ function exactCompanionCandidates(observation, skillName) {
26
26
  if (Array.isArray(observation)) {
27
- return observation.filter(candidate => candidate?.name === CORDIS_SKILL_NAME)
27
+ return observation.filter(candidate => candidate?.name === skillName)
28
28
  }
29
29
  if (observation === null || typeof observation !== 'object'
30
30
  || !Array.isArray(observation.candidates)) return observation
31
31
  return {
32
32
  ...observation,
33
- candidates: observation.candidates.filter(candidate => candidate?.name === CORDIS_SKILL_NAME),
33
+ candidates: observation.candidates.filter(candidate => candidate?.name === skillName),
34
34
  }
35
35
  }
36
36
 
37
- function exactCompanionProvider(provider) {
37
+ function exactCompanionProvider(provider, skillName) {
38
38
  if (provider === null || typeof provider !== 'object'
39
39
  || typeof provider.name !== 'string'
40
40
  || typeof provider.list !== 'function'
@@ -44,17 +44,17 @@ function exactCompanionProvider(provider) {
44
44
  return {
45
45
  ...provider,
46
46
  async list(options) {
47
- return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]))
47
+ return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]), skillName)
48
48
  },
49
49
  async get(candidate, options) {
50
- if (candidate?.name !== CORDIS_SKILL_NAME) return undefined
50
+ if (candidate?.name !== skillName) return undefined
51
51
  const skill = await Reflect.apply(provider.get, provider, [candidate, options])
52
- return skill?.name === CORDIS_SKILL_NAME ? skill : undefined
52
+ return skill?.name === skillName ? skill : undefined
53
53
  },
54
54
  }
55
55
  }
56
56
 
57
- function exactCompanionSkillPlugin(skillFilesystemPlugin) {
57
+ function exactCompanionSkillPlugin(skillFilesystemPlugin, skillName) {
58
58
  return {
59
59
  ...skillFilesystemPlugin,
60
60
  apply(pluginCtx, config) {
@@ -68,7 +68,7 @@ function exactCompanionSkillPlugin(skillFilesystemPlugin) {
68
68
  const facade = new Proxy(skills, {
69
69
  get(target, property) {
70
70
  if (property === 'registerProvider') {
71
- return create => target.registerProvider(control => exactCompanionProvider(create(control)))
71
+ return create => target.registerProvider(control => exactCompanionProvider(create(control), skillName))
72
72
  }
73
73
  const value = Reflect.get(target, property, target)
74
74
  return typeof value === 'function' ? value.bind(target) : value
@@ -144,12 +144,13 @@ function requireCompanionServices(agent) {
144
144
  return { agentPresets, skills }
145
145
  }
146
146
 
147
- function containsCompanionSkill(directory, fileExists) {
148
- return fileExists(join(directory, CORDIS_SKILL_NAME, 'SKILL.md'))
147
+ function companionSkill(directory, fileExists) {
148
+ const skillName = COMPANION_SKILL_NAMES.find(name => fileExists(join(directory, name, 'SKILL.md')))
149
+ return skillName === undefined ? undefined : { skillRoot: directory, skillName }
149
150
  }
150
151
 
151
152
  /** Resolve the official Cordis companion Skill across DSH preset generations. */
152
- export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
153
+ export async function resolveCompanionSkillResource(agentPresets, options = {}) {
153
154
  const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
154
155
  const resolution = typeof options === 'function' ? options() : options
155
156
  const fileExists = resolution.fileExists ?? existsSync
@@ -161,10 +162,11 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
161
162
  throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
162
163
  }
163
164
  const directory = join(dirname(preset.path), 'skills')
164
- if (!containsCompanionSkill(directory, fileExists)) {
165
- throw new Error(`ptc-plus: DSH cordis preset does not contain ${CORDIS_SKILL_NAME}/SKILL.md beside its composition`)
165
+ const resource = companionSkill(directory, fileExists)
166
+ if (resource === undefined) {
167
+ throw new Error(`ptc-plus: DSH cordis preset does not contain a supported companion SKILL.md beside its composition`)
166
168
  }
167
- return directory
169
+ return resource
168
170
  }
169
171
 
170
172
  for (const layout of COMPANION_PACKAGE_LAYOUTS) {
@@ -174,7 +176,8 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
174
176
  throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
175
177
  }
176
178
  const directory = layout.skillRoot(pkg.dir)
177
- if (containsCompanionSkill(directory, fileExists)) return directory
179
+ const resource = companionSkill(directory, fileExists)
180
+ if (resource !== undefined) return resource
178
181
  }
179
182
 
180
183
  if (resolution.packageOf === undefined && resolution.resolveLegacyPackageManifest !== undefined) {
@@ -186,29 +189,30 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
186
189
  }
187
190
  if (manifest !== undefined) {
188
191
  const directory = join(dirname(manifest), 'presets', CORDIS_PRESET_ID, 'skills')
189
- if (containsCompanionSkill(directory, fileExists)) return directory
192
+ const resource = companionSkill(directory, fileExists)
193
+ if (resource !== undefined) return resource
190
194
  }
191
195
  }
192
- throw new Error(`ptc-plus: the active DSH installation does not publish a loadable ${CORDIS_SKILL_NAME} companion Skill through its package-resolution surface`)
196
+ throw new Error(`ptc-plus: the active DSH installation does not publish a loadable official companion Skill through its package-resolution surface`)
193
197
  }
194
198
 
195
- async function requireCompanionSkill(agent, skills) {
199
+ async function requireCompanionSkill(agent, skills, skillName) {
196
200
  const lookup = {
197
201
  cwd: agent.session?.header?.cwd,
198
202
  scope: agent,
199
203
  }
200
204
  const owned = (await skills.list(lookup)).filter(skill => skill?.provider === CORDIS_SKILL_PROVIDER)
201
205
  const summary = owned[0]
202
- if (owned.length !== 1 || summary?.name !== CORDIS_SKILL_NAME
206
+ if (owned.length !== 1 || summary?.name !== skillName
203
207
  || summary.invocation?.modelInvocable !== true) {
204
- throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(CORDIS_SKILL_NAME)} as model-invocable`)
208
+ throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(skillName)} as model-invocable`)
205
209
  }
206
- const skill = await skills.get(CORDIS_SKILL_NAME, lookup)
210
+ const skill = await skills.get(skillName, lookup)
207
211
  if (skill?.provider !== CORDIS_SKILL_PROVIDER
208
212
  || skill.invocation?.modelInvocable !== true
209
213
  || typeof skill.content !== 'string'
210
214
  || skill.content.length === 0) {
211
- throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not loadable`)
215
+ throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(skillName)} is not loadable`)
212
216
  }
213
217
  }
214
218
 
@@ -321,7 +325,6 @@ export function createCordisToolsOwner(
321
325
  const withdrawn = new Map()
322
326
  const cordisInspectLeases = createCordisInspectLeases(ctx)
323
327
  const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
324
- const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
325
328
  const packageBaseUrl = ctx.baseUrl
326
329
  const skillResolution = companionResolution ?? (() => {
327
330
  const pluginPackages = ctx.get?.('pluginPackages')
@@ -423,13 +426,14 @@ export function createCordisToolsOwner(
423
426
  mounts.set(agent, mount)
424
427
  mount.activation = Promise.resolve().then(async () => {
425
428
  const { agentPresets, skills } = requireCompanionServices(agent)
426
- const skillDirectory = await resolveCompanionSkillDirectory(agentPresets, skillResolution)
429
+ const { skillRoot, skillName } = await resolveCompanionSkillResource(agentPresets, skillResolution)
430
+ const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin, skillName)
427
431
  if (mount.disposed) return
428
432
 
429
433
  const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
430
434
  providerName: CORDIS_SKILL_PROVIDER,
431
435
  includeDefaultRoots: false,
432
- bundledSkillDir: skillDirectory,
436
+ bundledSkillDir: skillRoot,
433
437
  watch: false,
434
438
  })
435
439
  if (typeof skillFiber?.dispose !== 'function') {
@@ -449,7 +453,7 @@ export function createCordisToolsOwner(
449
453
  await Promise.resolve(cordisFiber)
450
454
  if (mount.disposed) return
451
455
 
452
- await requireCompanionSkill(agent, skills)
456
+ await requireCompanionSkill(agent, skills, skillName)
453
457
  if (mount.disposed) return
454
458
  mount.activated = true
455
459
  }).catch(async error => {
@@ -94,19 +94,16 @@ export function createExecutionSeam(service, serviceName) {
94
94
  requireCompatible()
95
95
  return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
96
96
  },
97
- /**
98
- * Release one owner's retained environment. The resources live in the
99
- * original generation (the session kernel releases its state through its
100
- * own lifecycle). Adapter retirement only closes execution; the original
101
- * still owns cleanup for references retained by its consumers.
102
- */
103
- release(owner) {
104
- if (typeof service.release !== 'function') return Promise.resolve()
105
- try {
106
- return Promise.resolve(service.release.call(service, owner))
107
- } catch (error) {
108
- return Promise.reject(error)
109
- }
97
+ /** Release both the installed environment and any original-provider state. */
98
+ async release(owner) {
99
+ const entry = installed
100
+ const results = await Promise.allSettled([
101
+ Promise.resolve().then(() => entry?.release?.(owner)),
102
+ Promise.resolve().then(() => service.release?.call(service, owner)),
103
+ ])
104
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
105
+ if (failures.length === 1) throw failures[0]
106
+ if (failures.length > 1) throw new AggregateError(failures, 'ptc-plus: execution environment release failed')
110
107
  },
111
108
  [PLUGIN_EXECUTION_SEAM]() { return seam },
112
109
  })
@@ -143,11 +140,12 @@ export function createExecutionSeam(service, serviceName) {
143
140
  requireCompatible()
144
141
  return upstreamRun.call(service, spec)
145
142
  },
146
- installExecute(execute) {
143
+ installExecute(execute, release) {
147
144
  requireAvailable()
148
145
  if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
149
146
  if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
150
- const entry = { execute, language: service.language }
147
+ if (release !== undefined && typeof release !== 'function') throw new TypeError('ptc-plus: release entry must be a function')
148
+ const entry = { execute, release, language: service.language }
151
149
  installed = entry
152
150
  const restore = () => {
153
151
  if (installed !== entry) return
@@ -697,7 +697,13 @@ function applyEditableSlots(definition, promptText, slotPatches) {
697
697
  .finally(projected.release)
698
698
  }
699
699
 
700
- const releaseSeam = seam.installExecute(patchedRun)
700
+ const releaseSeam = seam.installExecute(patchedRun, owner => {
701
+ if (owner?.session === undefined || sessionId(owner) === undefined) return
702
+ // Host uses release for explicit reset and for its retained-environment effect.
703
+ // Cordis clears the public Agent scope uid before invoking disposal effects.
704
+ if (owner.ctx?.fiber?.uid === null) return sessions.disposeAgent(sessionId(owner))
705
+ return sessions.resetEnvironment(owner.session)
706
+ })
701
707
 
702
708
  /**
703
709
  * Whether this execution is the derived execution the edit transport submitted.
@@ -1,3 +1,4 @@
1
+ import { ENVIRONMENT_RESET_VERSION } from './session-journal-schema.js'
1
2
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
2
3
  import {
3
4
  CATALOG_PREFIX, EMPTY_CATALOG, LEGACY_STATE_PREFIX, PREVIOUS_EMPTY_CATALOG, PREVIOUS_STATE_PREFIX,
@@ -185,3 +186,10 @@ export function runtimeNoticeMessage(tip) {
185
186
  if (readRuntimeMessage(message) === undefined) throw new Error('invalid PTC recovery notice')
186
187
  return message
187
188
  }
189
+
190
+ /** A persisted computation boundary; the Host separately owns its reset receipt. */
191
+ export function environmentResetMessage() {
192
+ return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'environment-reset', version: ENVIRONMENT_RESET_VERSION },
193
+ content: [{ type: 'text', text: 'PTC Plus computation reset: earlier session bindings, named checkpoints and edit targets are retired. New computation starts empty with the configured global bindings. Completed external effects remain.' }],
194
+ })
195
+ }
@@ -7,6 +7,8 @@ import {
7
7
  JOURNAL_KEY,
8
8
  RECOVERY_BOUNDARY_KEY,
9
9
  RECOVERY_BOUNDARY_EVENT,
10
+ ENVIRONMENT_RESET_VERSION,
11
+ isEnvironmentResetEvent,
10
12
  DIAGNOSTIC_WORDING_KEY,
11
13
  DIAGNOSTIC_WORDING_FIELDS,
12
14
  SETTLEMENT_PROOF_KEY,
@@ -40,7 +42,7 @@ import {
40
42
 
41
43
  const RECOVERY_BOUNDARY_EVIDENCE = Symbol('recoveryBoundaryEvidence')
42
44
  const RUN_SELECTION_CLEAR_REASONS = new Set([
43
- 'turn/start', 'turn/end', 'duplicate-call-sequence', 'invalid-source-relation',
45
+ 'turn/start', 'turn/end', 'environment-reset', 'duplicate-call-sequence', 'invalid-source-relation',
44
46
  'ambiguous-call-id', 'ambiguous-call-sequence', 'duplicate-result', 'result-identity-mismatch',
45
47
  ])
46
48
  const MODEL_VISIBLE_SURFACE_EVENTS = new Set([
@@ -80,8 +82,12 @@ function sourceForRunCall(call, executableCalls) {
80
82
  /** Select the boundary of a failed replay node or an unavailable recovered suffix. */
81
83
  export function recoveryBoundaryForHistory(history, failedNode, { reset = false } = {}) {
82
84
  if (reset) failedNode = history.nodes[0]
85
+ const unavailable = history.available === false ? history.volatileSuffix[0] : undefined
86
+ if (failedNode === undefined && unavailable?.kind === 'environment-reset') {
87
+ return { failedResetSeq: unavailable.seq, frontierCallSeq: null }
88
+ }
83
89
  const failedCallSeq = failedNode === undefined
84
- ? history.available === false ? history.volatileSuffix[0]?.seq : undefined
90
+ ? unavailable?.seq
85
91
  : failedNode.callSeq
86
92
  if (failedCallSeq === undefined) return undefined
87
93
  const frontier = reset ? undefined : failedNode === undefined ? history.head : failedNode.parent
@@ -735,6 +741,7 @@ function correlationEvent(event) {
735
741
  ...(typeof event.data?.message?.source?.callId === 'string'
736
742
  ? { callId: event.data.message.source.callId } : {}),
737
743
  } } } } : {}),
744
+ ...(isEnvironmentResetEvent(event) ? { data: { source: event.data.source } } : {}),
738
745
  ...(event.type === 'compaction/prune' ? { data: {
739
746
  ...(event.data?.shadowedSeqs === undefined ? {} : { shadowedSeqs: event.data.shadowedSeqs }),
740
747
  ...(event.data?.shadowedRange === undefined ? {} : { shadowedRange: event.data.shadowedRange }),
@@ -1158,7 +1165,7 @@ export function validateSessionTimelineState(value, logFacts, options = {}) {
1158
1165
  'ordinaryResultSeqs', 'settledResultsByEventSeq', 'scope', 'latestRun', 'editableRun', 'lastSuccessfulRunIndex',
1159
1166
  'unavailableResultSeq', 'unavailableResultReason', 'pruneReplacementWindow', 'runSelectionFrontier',
1160
1167
  'runSelectionHistory', 'lastEventIndex', 'eventFacts', 'retainedBytes', 'retentionSummary', 'compacted',
1161
- 'hotCellDropped']) && typeof value.openTurn === 'boolean'
1168
+ 'hotCellDropped', 'environmentReset']) && typeof value.openTurn === 'boolean'
1162
1169
  && typeof value.found === 'boolean' && isCanonicalSequence(value.scope)
1163
1170
  && typeof value.retainedBytes === 'number' && value.retainedBytes >= 0
1164
1171
  && (value.retentionSummary === undefined || value.retentionSummary === null || isRecord(value.retentionSummary))
@@ -1328,9 +1335,10 @@ export function validateSessionTimelineState(value, logFacts, options = {}) {
1328
1335
  && isCanonicalSequence(observation.eventIndex) && observation.eventIndex > correlationIndex
1329
1336
  && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
1330
1337
  && isRecord(observation.event) && isCanonicalSequence(observation.event.seq)
1331
- && ['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
1338
+ && ['turn/start', 'turn/end', 'user/message', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
1332
1339
  'tool/ptc-dispatch-start', 'tool/ptc-dispatch']
1333
1340
  .includes(observation.event.type)
1341
+ && (observation.event.type !== 'user/message' || isEnvironmentResetEvent(observation.event))
1334
1342
  && deepEqualJson(correlationEvent(observation.event), observation.event)
1335
1343
  && (observation.event.type === 'tool/ptc-dispatch-start'
1336
1344
  && observation.event.data?.name === 'edit_run_code'
@@ -1363,6 +1371,9 @@ export function validateSessionTimelineState(value, logFacts, options = {}) {
1363
1371
  }
1364
1372
  reconstructed = replaySessionTimelineFact(reconstructed, observation, executable)
1365
1373
  }
1374
+ const resetFact = value.eventFacts.findLast(fact => isEnvironmentResetEvent(fact.event))
1375
+ requireFact(deepEqualJson(value.environmentReset ?? null, resetFact === undefined ? null
1376
+ : { seq: resetFact.event.seq, index: resetFact.eventIndex }))
1366
1377
  const summary = retentionSummaryOf(value)
1367
1378
  // A compacted state that lost its summary must be rejected: dropping the summary
1368
1379
  // would erase the only trace of the clusters the window removed.
@@ -1428,7 +1439,8 @@ export function validateSessionTimelineState(value, logFacts, options = {}) {
1428
1439
  if (item.frontier !== undefined) {
1429
1440
  dispatchFrontier = item.frontier
1430
1441
  dispatchRun = undefined
1431
- if (item.frontier.reason === 'turn/start' || item.frontier.reason === 'turn/end') {
1442
+ if (item.frontier.reason === 'turn/start' || item.frontier.reason === 'turn/end'
1443
+ || item.frontier.reason === 'environment-reset') {
1432
1444
  dispatchScope = item.frontier.scope
1433
1445
  dispatchClaims.clear()
1434
1446
  dispatchEdits.clear()
@@ -1669,13 +1681,14 @@ function requireRecordedSettlementProof(meta, journal, isError = undefined) {
1669
1681
  }
1670
1682
 
1671
1683
  function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, retainedCalls) {
1684
+ if (inputEvent?.type === 'user/message' && !isEnvironmentResetEvent(inputEvent)) return previous
1672
1685
  const nested = inputEvent?.type === 'tool/ptc-dispatch-start'
1673
1686
  || inputEvent?.type === 'tool/ptc-dispatch'
1674
1687
  if (nested && inputEvent.data?.name !== 'edit_run_code'
1675
1688
  && inputEvent.data?.name !== 'run_code'
1676
1689
  && !previous.eventFacts.some(fact => fact.event.type === 'tool/ptc-dispatch-start'
1677
1690
  && fact.event.data?.subCallId === inputEvent.data?.subCallId)) return previous
1678
- if (!['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
1691
+ if (!['turn/start', 'turn/end', 'user/message', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
1679
1692
  'tool/ptc-dispatch-start', 'tool/ptc-dispatch'].includes(inputEvent?.type)) {
1680
1693
  return previous
1681
1694
  }
@@ -1746,6 +1759,21 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
1746
1759
  resetTurn(false)
1747
1760
  continue
1748
1761
  }
1762
+ if (isEnvironmentResetEvent(event)) {
1763
+ state.environmentReset = { seq: event.seq, index: eventIndex }
1764
+ pendingByCallId.clear()
1765
+ seenCallIds.clear()
1766
+ claimedEditTargets.clear()
1767
+ editClaims.clear()
1768
+ delete state.unavailableResultSeq
1769
+ delete state.unavailableResultReason
1770
+ clearRunSelection('environment-reset')
1771
+ if (event.data.source.version !== ENVIRONMENT_RESET_VERSION) {
1772
+ state.unavailableResultSeq = isCanonicalSequence(event.seq) ? event.seq : eventIndex
1773
+ state.unavailableResultReason = { seq: state.unavailableResultSeq, reason: 'invalid PTC environment reset boundary' }
1774
+ }
1775
+ continue
1776
+ }
1749
1777
  if (event?.type === RECOVERY_BOUNDARY_EVENT) {
1750
1778
  state.unavailableResultSeq ??= isCanonicalSequence(event.seq) ? event.seq : eventIndex
1751
1779
  continue
@@ -1824,6 +1852,7 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
1824
1852
  settledResultsByEventSeq.set(event.seq, republishedSettlement)
1825
1853
  continue
1826
1854
  }
1855
+ if (entry?.event?.seq <= (state.environmentReset?.seq ?? -1)) continue
1827
1856
  if (hasSourceRelation && !canonicalSourceRelation) {
1828
1857
  const callSeq = invalidSourceCallSeq(event, state.executableCalls, pendingByCallId)
1829
1858
  if (callSeq !== undefined) {
@@ -2387,29 +2416,33 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2387
2416
  const needsPayload = [...folded.executableCalls.values()]
2388
2417
  .some(entry => typeof entry?.event?.data?.arguments !== 'string')
2389
2418
  const timeline = needsPayload ? withFullRetention(() => foldSessionTimeline(events)) : folded
2390
- const calls = timelineCalls(timeline).filter(call => call.seq !== currentCallSeq)
2391
- const { executableCalls, results, found } = timeline
2419
+ const resetSeq = timeline.environmentReset?.seq ?? -1
2420
+ const calls = timelineCalls(timeline).filter(call => call.seq !== currentCallSeq && call.seq > resetSeq)
2421
+ const executableCalls = new Map([...timeline.executableCalls].filter(([seq]) => seq > resetSeq))
2422
+ const results = new Map([...timeline.results].filter(([seq]) => seq > resetSeq))
2423
+ const found = timeline.found || resetSeq >= 0
2392
2424
  const visibleCalls = options?.visibleCallSeqs
2393
2425
  const appliedBoundaries = options?.[RECOVERY_BOUNDARY_EVIDENCE]
2394
2426
  const invalidCallSeqs = new Set()
2395
2427
  let unavailableBoundary
2396
- const registerUnavailable = ({ seq, reason, code }, recoveryState) => {
2428
+ const registerUnavailable = ({ seq, reason, code, kind }, recoveryState) => {
2397
2429
  if (unavailableBoundary === undefined || seq <= unavailableBoundary.seq) {
2398
- unavailableBoundary = { seq, reason }
2430
+ unavailableBoundary = { seq, reason, ...(kind === undefined ? {} : { kind }) }
2399
2431
  }
2400
2432
  if (recoveryState === undefined) return
2401
2433
  invalidCallSeqs.add(seq)
2402
2434
  recoveryState.trusted = false
2403
2435
  if (!recoveryState.volatileSuffix.some(item => item.seq === seq)) {
2404
- const item = { seq, code, reason }
2436
+ const item = { seq, code, reason, ...(kind === undefined ? {} : { kind }) }
2405
2437
  const index = recoveryState.volatileSuffix.findIndex(existing => existing.seq > seq)
2406
2438
  if (index < 0) recoveryState.volatileSuffix.push(item)
2407
2439
  else recoveryState.volatileSuffix.splice(index, 0, item)
2408
2440
  }
2409
2441
  }
2410
- if (timeline.unavailableResultSeq !== undefined) {
2442
+ if (timeline.unavailableResultSeq !== undefined && timeline.unavailableResultSeq >= resetSeq) {
2411
2443
  registerUnavailable({
2412
2444
  seq: timeline.unavailableResultSeq,
2445
+ ...(timeline.unavailableResultSeq === resetSeq ? { kind: 'environment-reset' } : {}),
2413
2446
  reason: timeline.unavailableResultReason?.seq === timeline.unavailableResultSeq
2414
2447
  ? timeline.unavailableResultReason.reason
2415
2448
  : 'unavailable or malformed dsh-ptc-plus journal result',
@@ -2428,7 +2461,7 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2428
2461
  normalizedExtraBoundaries = []
2429
2462
  }
2430
2463
  const boundaries = [
2431
- ...timeline.boundaries,
2464
+ ...recoveryBoundariesAfterReset(timeline),
2432
2465
  ...normalizedExtraBoundaries.map(boundary => ({
2433
2466
  ...boundary,
2434
2467
  eventSeq: Number.POSITIVE_INFINITY,
@@ -2442,7 +2475,7 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2442
2475
  for (const [resultSeq, { journal, eventIndex }] of results.entries()) {
2443
2476
  const candidateConfirms = []
2444
2477
  for (const callSeq of journal?.confirms ?? []) {
2445
- if (!isConfirmableNoop(timeline, callSeq, eventIndex)) {
2478
+ if (callSeq <= resetSeq || !isConfirmableNoop(timeline, callSeq, eventIndex)) {
2446
2479
  const reason = `confirmed no-op does not identify an earlier unjournaled run_code call seq ${callSeq}`
2447
2480
  invalidResultReasons.set(resultSeq, reason)
2448
2481
  registerUnavailable({ seq: resultSeq, reason })
@@ -2470,9 +2503,10 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2470
2503
  ))
2471
2504
  const rejectBoundary = (boundary, reason) => {
2472
2505
  const carrierCallSeq = carrierCallSeqForBoundary(boundary)
2506
+ const failedSeq = boundary.failedResetSeq ?? boundary.failedCallSeq
2473
2507
  const contractionSeq = carrierCallSeq === undefined
2474
- ? boundary.failedCallSeq
2475
- : Math.min(boundary.failedCallSeq, carrierCallSeq)
2508
+ ? failedSeq
2509
+ : Math.min(failedSeq, carrierCallSeq)
2476
2510
  // A rejected boundary still establishes a conservative contraction point:
2477
2511
  // the same window an accepted boundary would contract stays unproved, so
2478
2512
  // settlements folded after this rejection cannot re-enter the frontier.
@@ -2486,12 +2520,13 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2486
2520
  ...foldOptions,
2487
2521
  surfaceContracted: state.surfaceContracted,
2488
2522
  })
2489
- registerUnavailable({ seq: carrierCallSeq ?? boundary.failedCallSeq, reason }, state)
2523
+ registerUnavailable({ seq: carrierCallSeq ?? failedSeq, reason }, state)
2490
2524
  }
2491
2525
  const applyBoundariesBefore = (seq) => {
2492
2526
  while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq <= seq) {
2493
2527
  const boundary = boundaries[boundaryIndex++]
2494
2528
  const carrierCallSeq = carrierCallSeqForBoundary(boundary)
2529
+ const failedSeq = boundary.failedResetSeq ?? boundary.failedCallSeq
2495
2530
  const failedCall = executableCalls.get(boundary.failedCallSeq)
2496
2531
  // Derived records exist only after the shared timeline has proved both
2497
2532
  // dispatch pairs and the final root publication. A raw nested start does
@@ -2503,14 +2538,17 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2503
2538
  )) : []
2504
2539
  const failedExecution = failedCall === undefined && derivedRecords.length === 1
2505
2540
  ? derivedRecords[0] : undefined
2541
+ const failedReset = boundary.failedResetSeq === resetSeq
2542
+ && unavailableBoundary?.seq === resetSeq && unavailableBoundary.kind === 'environment-reset'
2543
+ && resetSeq < boundary.eventSeq
2506
2544
  const isEarlierExecution = failedCall === undefined
2507
2545
  ? failedExecution !== undefined && recordEventSeq(failedExecution) < boundary.eventSeq
2508
2546
  : failedCall.event.seq < boundary.eventSeq
2509
- if (!isEarlierExecution) {
2547
+ if (boundary.failedResetSeq !== undefined ? !failedReset : !isEarlierExecution) {
2510
2548
  rejectBoundary(boundary, 'recovery boundary does not identify an earlier executable call')
2511
2549
  continue
2512
2550
  }
2513
- const failedIndex = state.nodes.findIndex(node => node.callSeq === boundary.failedCallSeq)
2551
+ const failedIndex = state.nodes.findIndex(node => node.callSeq === failedSeq)
2514
2552
  const expectedFrontier = failedIndex < 0 ? state.head : state.nodes[failedIndex].parent
2515
2553
  const frontierIndex = boundary.frontierCallSeq === null
2516
2554
  ? undefined
@@ -2519,25 +2557,25 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2519
2557
  rejectBoundary(boundary, 'recovery boundary frontier does not match the failed call parent')
2520
2558
  continue
2521
2559
  }
2522
- const resolvesUnavailable = unavailableBoundary?.seq === boundary.failedCallSeq
2560
+ const resolvesUnavailable = unavailableBoundary?.seq === failedSeq
2523
2561
  const resolvesConfirmedNoop = confirmedNoops.has(boundary.failedCallSeq)
2524
2562
  if (failedIndex < 0 && !resolvesUnavailable && !resolvesConfirmedNoop) {
2525
2563
  rejectBoundary(boundary, 'recovery boundary failed call is outside the verified frontier')
2526
2564
  continue
2527
2565
  }
2528
- if (carrierCallSeq !== undefined && carrierCallSeq <= boundary.failedCallSeq) {
2566
+ if (carrierCallSeq !== undefined && carrierCallSeq <= failedSeq) {
2529
2567
  rejectBoundary(boundary, 'recovery boundary carrier does not follow the failed call')
2530
2568
  continue
2531
2569
  }
2532
2570
  if (Array.isArray(appliedBoundaries)) appliedBoundaries.push(boundary)
2533
2571
  if (carrierCallSeq !== undefined) {
2534
- contractedCallRanges.push({ start: boundary.failedCallSeq, end: carrierCallSeq })
2572
+ contractedCallRanges.push({ start: failedSeq, end: carrierCallSeq })
2535
2573
  }
2536
2574
  for (let index = 0; index < state.nodes.length; index += 1) {
2537
2575
  if (dependsOn(state.nodes, index, failedIndex)) invalidCallSeqs.add(state.nodes[index].callSeq)
2538
2576
  }
2539
2577
  for (const record of records) {
2540
- if ((failedIndex < 0 && record.call.seq >= boundary.failedCallSeq)
2578
+ if ((failedIndex < 0 && record.call.seq >= failedSeq)
2541
2579
  || isContractedCallSeq(record.call.seq)) {
2542
2580
  invalidCallSeqs.add(record.call.seq)
2543
2581
  }
@@ -2663,7 +2701,12 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
2663
2701
  return history
2664
2702
  }
2665
2703
 
2666
- /** Prove that recovery consumes every persisted boundary at its declared frontier. */
2704
+ function recoveryBoundariesAfterReset(timeline) {
2705
+ const resetSeq = timeline.environmentReset?.seq ?? -1
2706
+ return timeline.boundaries.filter(boundary => boundary.carrierCallSeq > resetSeq)
2707
+ }
2708
+
2709
+ /** Prove every persisted boundary eligible after the latest reset at its declared frontier. */
2667
2710
  export function validateRecoveryBoundaryApplication(events) {
2668
2711
  const applied = []
2669
2712
  const recovered = recoverJournal(
@@ -2671,7 +2714,7 @@ export function validateRecoveryBoundaryApplication(events) {
2671
2714
  undefined,
2672
2715
  { [RECOVERY_BOUNDARY_EVIDENCE]: applied },
2673
2716
  )
2674
- const declared = foldSessionTimeline(events).boundaries.length
2717
+ const declared = recoveryBoundariesAfterReset(foldSessionTimeline(events)).length
2675
2718
  if (!recovered.available || applied.length !== declared) {
2676
2719
  throw new Error('migrated PTC recovery boundary does not prove its declared frontier')
2677
2720
  }
@@ -1,3 +1,4 @@
1
+ import { isPtcMessageSource } from './message-sources.js'
1
2
  import {
2
3
  LEGACY_DEFAULT_EXPORT_BINDING,
3
4
  LIVE_DEFAULT_EXPORT_BINDING,
@@ -29,6 +30,14 @@ export const PREVIOUS_JOURNAL_VERSION = 3
29
30
  export const USER_BINDING_RELATIONLESS_JOURNAL_VERSION = 4
30
31
  export const FINGERPRINT_REUSE_JOURNAL_VERSION = 5
31
32
  export const VERSIONED_BINDING_REUSE_JOURNAL_VERSION = 6
33
+ export const ENVIRONMENT_RESET_VERSION = 1
34
+
35
+ /** Structured producer identity proves a reset independently of notice wording. */
36
+ export function isEnvironmentResetEvent(event) {
37
+ const source = event?.data?.source
38
+ return event?.type === 'user/message' && isPtcMessageSource(source)
39
+ && source.form === 'environment-reset'
40
+ }
32
41
  export const RECOVERY_BOUNDARY_EVENT = 'ptc-plus/recovery-boundary'
33
42
 
34
43
  export const STATUSES = new Set(['durable', 'volatile', 'discarded', 'noop'])
@@ -121,6 +130,7 @@ export function normalizeJournalUserBindingNames(journal) {
121
130
  return normalizeUserBindingNames(journal.userBindingNames)
122
131
  }
123
132
  export const RECOVERY_BOUNDARY_FIELDS = new Set(['failedCallSeq', 'frontierCallSeq'])
133
+ export const RESET_RECOVERY_BOUNDARY_FIELDS = new Set(['failedResetSeq', 'frontierCallSeq'])
124
134
  export const REPL_TOOL_NAMES = new Set(['run_code', 'edit_run_code'])
125
135
  export const REWRITE_FIELDS = new Set(['kind', 'description', 'source'])
126
136
  export const REWRITE_KINDS = new Set(['import', 'redeclaration', 'export'])
@@ -63,6 +63,8 @@ import {
63
63
  JOURNAL_VERSIONS,
64
64
  USER_BINDINGS_REUSE_POLICIES,
65
65
  RECOVERY_BOUNDARY_FIELDS,
66
+ RESET_RECOVERY_BOUNDARY_FIELDS,
67
+ isEnvironmentResetEvent,
66
68
  DIAGNOSTIC_WORDING_KEY,
67
69
  SETTLEMENT_PROOF_KEY,
68
70
  SETTLEMENT_PROOF_FIELDS,
@@ -722,18 +724,19 @@ export function recoveryBoundariesEqual(left, right) {
722
724
  }
723
725
 
724
726
  function normalizeRecoveryBoundaryValue(value, eventSeq = undefined) {
727
+ const reset = isRecord(value) && Object.hasOwn(value, 'failedResetSeq')
725
728
  if (!isRecord(value)
726
- || !isCanonicalSequence(value.failedCallSeq)
727
- || (value.frontierCallSeq !== null
728
- && !isCanonicalSequence(value.frontierCallSeq))) {
729
+ || !isCanonicalSequence(reset ? value.failedResetSeq : value.failedCallSeq)
730
+ || (reset ? value.frontierCallSeq !== null
731
+ : value.frontierCallSeq !== null && !isCanonicalSequence(value.frontierCallSeq))) {
729
732
  throw new Error('invalid dsh-ptc-plus recovery boundary')
730
733
  }
731
- assertOwnFields(value, RECOVERY_BOUNDARY_FIELDS, 'recovery boundary')
734
+ assertOwnFields(value, reset ? RESET_RECOVERY_BOUNDARY_FIELDS : RECOVERY_BOUNDARY_FIELDS, 'recovery boundary')
732
735
  if (eventSeq !== undefined && !isCanonicalSequence(eventSeq)) {
733
736
  throw new Error('invalid dsh-ptc-plus recovery boundary event sequence')
734
737
  }
735
738
  return {
736
- failedCallSeq: value.failedCallSeq,
739
+ ...(reset ? { failedResetSeq: value.failedResetSeq } : { failedCallSeq: value.failedCallSeq }),
737
740
  frontierCallSeq: value.frontierCallSeq,
738
741
  ...(eventSeq === undefined ? {} : { eventSeq }),
739
742
  }
@@ -1111,15 +1114,24 @@ export function migrateRecoveryBoundaryEvents(events) {
1111
1114
  }
1112
1115
  return target.retainedSeq
1113
1116
  }
1114
- const mapBoundary = (boundary, resultSeq) => ({
1115
- failedCallSeq: mapCall(boundary.failedCallSeq, resultSeq, 'recovery-boundary failure'),
1116
- frontierCallSeq: boundary.frontierCallSeq === null ? null : (() => {
1117
- if (boundary.frontierCallSeq >= boundary.failedCallSeq) {
1118
- throw new Error('recovery-boundary frontier must precede its failed call')
1117
+ const mapBoundary = (boundary, resultSeq) => {
1118
+ if (boundary.failedResetSeq !== undefined) {
1119
+ const target = sequenceMap.get(boundary.failedResetSeq)
1120
+ if (!isEnvironmentResetEvent(target?.event) || boundary.failedResetSeq >= resultSeq) {
1121
+ throw new Error('unproved recovery-boundary reset event reference')
1119
1122
  }
1120
- return mapCall(boundary.frontierCallSeq, resultSeq, 'recovery-boundary frontier')
1121
- })(),
1122
- })
1123
+ return { failedResetSeq: mapEvent(boundary.failedResetSeq, 'recovery-boundary reset'), frontierCallSeq: null }
1124
+ }
1125
+ return {
1126
+ failedCallSeq: mapCall(boundary.failedCallSeq, resultSeq, 'recovery-boundary failure'),
1127
+ frontierCallSeq: boundary.frontierCallSeq === null ? null : (() => {
1128
+ if (boundary.frontierCallSeq >= boundary.failedCallSeq) {
1129
+ throw new Error('recovery-boundary frontier must precede its failed call')
1130
+ }
1131
+ return mapCall(boundary.frontierCallSeq, resultSeq, 'recovery-boundary frontier')
1132
+ })(),
1133
+ }
1134
+ }
1123
1135
  const migrated = []
1124
1136
  const pending = []
1125
1137
  const boundaryOrigins = new Map()