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.
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/client.cjs +13 -23
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +18 -12
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +2 -0
- package/docs/adr/0051-persist-explicit-host-environment-resets.md +29 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -2
- package/docs/durability-design.md +10 -0
- package/docs/installation.md +2 -2
- package/docs/releases/v0.5.6.md +7 -3
- package/docs/releases/v0.5.7.md +51 -0
- package/docs/runtime-reference.md +4 -4
- package/internal/config-spec.js +2 -2
- package/internal/cordis-tools-owner.js +32 -28
- package/internal/execution-seam-compat.js +13 -15
- package/internal/runtime-bridge-owner.js +7 -1
- package/internal/runtime-messages.js +8 -0
- package/internal/session-journal-recovery.js +68 -25
- package/internal/session-journal-schema.js +10 -0
- package/internal/session-journal.js +25 -13
- package/internal/session-log-view.js +15 -8
- package/internal/session-runtime.js +13 -0
- package/package.json +1 -1
- package/src/client-activity.js +4 -1
- package/src/client-settings-view.js +3 -10
|
@@ -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
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
50
|
+
if (candidate?.name !== skillName) return undefined
|
|
51
51
|
const skill = await Reflect.apply(provider.get, provider, [candidate, options])
|
|
52
|
-
return skill?.name ===
|
|
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
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
2391
|
-
const
|
|
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
|
|
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
|
-
?
|
|
2475
|
-
: Math.min(
|
|
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 ??
|
|
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 ===
|
|
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 ===
|
|
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 <=
|
|
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:
|
|
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 >=
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
if (boundary.
|
|
1118
|
-
throw new Error('recovery-boundary
|
|
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
|
|
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()
|