dsh-ptc-plus 0.5.1 → 0.5.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/README.en.md +7 -3
- package/README.md +7 -3
- package/client.cjs +1467 -84
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/architecture.md +2 -2
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +7 -1
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +36 -2
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +36 -15
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-environment.js +162 -10
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +113 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +79 -14
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/runtime-bridge-owner.js +1 -1
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1009 -99
- package/internal/session-journal-schema.js +7 -5
- package/internal/session-journal.js +177 -14
- package/internal/session-log-view.js +111 -24
- package/internal/session-runtime.js +674 -33
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +3 -0
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +3 -1
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +459 -24
- package/src/client-copy.js +163 -2
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +64 -2
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { PTC_STATE_NAMES, recoveryTipIdentity } from './runtime-messages.js'
|
|
2
|
+
import {
|
|
3
|
+
PROMPT_SLOT_IDS, TIP_BODY, TIP_BODY_PLATFORM_DETAILED, TIP_BODY_REPEATED_BINDING,
|
|
4
|
+
TIP_BODY_REPEATED_BINDING_CAPABILITY, TIP_DETAIL_BINDING, TIP_DETAIL_CAPABILITY,
|
|
5
|
+
TIP_PREFIX_PLATFORM_COMMAND, TIP_PREFIX_REPEATED_BINDING,
|
|
6
|
+
} from './prompt-text.js'
|
|
2
7
|
|
|
3
8
|
const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
|
|
4
9
|
const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
|
|
5
10
|
const PLATFORM_FACTS = '`return { platform: process.platform, arch: process.arch, node: process.version, cwd: process.cwd(), execPath: process.execPath }`'
|
|
11
|
+
/**
|
|
12
|
+
* The delivered prefixes, one slot per trigger kind.
|
|
13
|
+
*
|
|
14
|
+
* Tip identity comes from the notice name, so a prefix carries no recognizer
|
|
15
|
+
* contract; both are read-only registry literals so their bytes are accounted.
|
|
16
|
+
*/
|
|
6
17
|
const TIP_PREFIXES = Object.freeze({
|
|
7
|
-
'repeated-binding-failure':
|
|
8
|
-
'platform-command-failure':
|
|
18
|
+
'repeated-binding-failure': TIP_PREFIX_REPEATED_BINDING,
|
|
19
|
+
'platform-command-failure': TIP_PREFIX_PLATFORM_COMMAND,
|
|
9
20
|
})
|
|
10
21
|
|
|
11
22
|
// TODO(dsh-tips-api): replace this local provider with an adapter after dsh-tips publishes a stable facts and decision interface.
|
|
@@ -91,18 +102,46 @@ function tipCandidate(view) {
|
|
|
91
102
|
return undefined
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
const TIP_BODY_SLOTS = Object.freeze({
|
|
106
|
+
platform: PROMPT_SLOT_IDS.tipBodyPlatform,
|
|
107
|
+
platformDetailed: PROMPT_SLOT_IDS.tipBodyPlatformDetailed,
|
|
108
|
+
repeatedBinding: PROMPT_SLOT_IDS.tipBodyRepeatedBinding,
|
|
109
|
+
repeatedBindingCapability: PROMPT_SLOT_IDS.tipBodyRepeatedBindingCapability,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const TIP_DETAIL_SLOTS = Object.freeze({
|
|
113
|
+
binding: PROMPT_SLOT_IDS.tipDetailBinding,
|
|
114
|
+
capability: PROMPT_SLOT_IDS.tipDetailCapability,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const TIP_PREFIX_SLOTS = Object.freeze({
|
|
118
|
+
'repeated-binding-failure': PROMPT_SLOT_IDS.tipPrefixRepeatedBinding,
|
|
119
|
+
'platform-command-failure': PROMPT_SLOT_IDS.tipPrefixPlatformCommand,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Every delivered tip body reads its own registry slot; escalation only decides
|
|
124
|
+
* whether the `{detail}` hole carries its sentence, and which of the two registered
|
|
125
|
+
* detail slots fills it. Prefix and detail slots are read-only, so a customised body
|
|
126
|
+
* cannot move tip identity or escalation.
|
|
127
|
+
*/
|
|
128
|
+
function renderTip(id, detailed, candidate = {}, prompt) {
|
|
129
|
+
const body = (slotId, fallback) => prompt?.text(slotId) ?? fallback
|
|
130
|
+
const prefix = body(TIP_PREFIX_SLOTS[id], TIP_PREFIXES[id])
|
|
95
131
|
if (id === 'platform-command-failure') {
|
|
96
|
-
|
|
97
|
-
?
|
|
98
|
-
:
|
|
132
|
+
const template = detailed
|
|
133
|
+
? body(TIP_BODY_SLOTS.platformDetailed, TIP_BODY_PLATFORM_DETAILED)
|
|
134
|
+
: body(TIP_BODY_SLOTS.platform, TIP_BODY)
|
|
135
|
+
return `${prefix} ${template.replace('{platformFacts}', PLATFORM_FACTS)}`
|
|
99
136
|
}
|
|
100
137
|
if (!candidate.capability) {
|
|
101
|
-
|
|
138
|
+
const template = body(TIP_BODY_SLOTS.repeatedBinding, TIP_BODY_REPEATED_BINDING)
|
|
139
|
+
const detail = detailed ? body(TIP_DETAIL_SLOTS.binding, TIP_DETAIL_BINDING) : ''
|
|
140
|
+
return `${prefix} ${template.replace('{detail}', detail)}`
|
|
102
141
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
const template = body(TIP_BODY_SLOTS.repeatedBindingCapability, TIP_BODY_REPEATED_BINDING_CAPABILITY)
|
|
143
|
+
const detail = detailed ? body(TIP_DETAIL_SLOTS.capability, TIP_DETAIL_CAPABILITY) : ''
|
|
144
|
+
return `${prefix} ${template.replace('{detail}', detail)}`
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
export function latestRecoveryTip(view, config) {
|
|
@@ -117,6 +156,6 @@ export function latestRecoveryTip(view, config) {
|
|
|
117
156
|
if (!Number.isSafeInteger(ordinal)) return undefined
|
|
118
157
|
return {
|
|
119
158
|
name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
|
|
120
|
-
text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate),
|
|
159
|
+
text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate, config.prompt),
|
|
121
160
|
}
|
|
122
161
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EDIT_RUN_CODE_DESCRIPTION } from './prompt-text.js'
|
|
1
2
|
import { runInNewContext } from 'node:vm'
|
|
2
3
|
|
|
3
4
|
const DEFAULT_REGEX_TIMEOUT_MS = 100
|
|
@@ -252,10 +253,10 @@ function assemble(source, replacements) {
|
|
|
252
253
|
return changed ? { code: chunks.join('') } : { error: 'edits must change at least one matched fragment' }
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
export function editRunCodeSchema() {
|
|
256
|
+
export function editRunCodeSchema(description = EDIT_RUN_CODE_DESCRIPTION) {
|
|
256
257
|
return {
|
|
257
258
|
name: 'edit_run_code',
|
|
258
|
-
description
|
|
259
|
+
description,
|
|
259
260
|
parameters: {
|
|
260
261
|
type: 'object',
|
|
261
262
|
additionalProperties: false,
|
|
@@ -831,7 +831,7 @@ export function createRuntimeBridgeOwner({
|
|
|
831
831
|
&& deepEqualJson(meta?.[POST_EDIT_KEY], current.postEdit.provenance))
|
|
832
832
|
},
|
|
833
833
|
disposeAgent(agent) {
|
|
834
|
-
return sessions.
|
|
834
|
+
return sessions.disposeAgent(sessionId(agent) ?? String(agent.id))
|
|
835
835
|
},
|
|
836
836
|
disposeSession(session) {
|
|
837
837
|
return sessions.disposeSession(String(session.id))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { capabilityPolicyForConfig } from './capability-policy.js'
|
|
2
|
+
import { validatePromptTextDocument } from './prompt-text.js'
|
|
2
3
|
import {
|
|
3
4
|
CONFIG_DEFAULTS,
|
|
4
5
|
CONFIG_FIELDS,
|
|
@@ -117,6 +118,7 @@ export function resolveConfig(config = {}) {
|
|
|
117
118
|
}
|
|
118
119
|
for (const field of CONFIG_FIELDS) assertField(field, resolved[field.key])
|
|
119
120
|
validateCapabilityProfiles(JSON.parse(resolved.capabilityProfiles))
|
|
121
|
+
validatePromptTextDocument(resolved.promptText)
|
|
120
122
|
for (const [key, array] of [['capabilityBaseline', true], ['capabilityOverrides', false]]) {
|
|
121
123
|
let value
|
|
122
124
|
try { value = JSON.parse(resolved[key]) } catch { throw new TypeError(`ptc-plus: ${key} must contain valid JSON`) }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { latestRecoveryTip, runtimeHistoryForView } from './recovery-tips.js'
|
|
2
|
+
import { HOT_CELL_DROPPED, HOT_CELL_DROPPED_EN } from './projection-retention.js'
|
|
2
3
|
import { foldSurface, deriveEventMessage, snapshotSessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
4
|
import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections'
|
|
4
5
|
import { readSessionLog } from './session-events.js'
|
|
@@ -75,6 +76,12 @@ export async function handBackRuntimeSessions(ctx, owners) {
|
|
|
75
76
|
function continuationFeedback(view) {
|
|
76
77
|
const run = view.latestRun
|
|
77
78
|
const rewrites = run?.rewrites
|
|
79
|
+
if (run === undefined && view.hotCellDropped === true) {
|
|
80
|
+
// The newest cell lost its payload to the retention budget. Silence here reads as
|
|
81
|
+
// "nothing to advise", which is indistinguishable from a healthy session; tell the
|
|
82
|
+
// model that the evidence was removed rather than absent.
|
|
83
|
+
return `${HOT_CELL_DROPPED} / ${HOT_CELL_DROPPED_EN} Inspect the tool result of the dropped cell before continuing; do not replay it automatically.`
|
|
84
|
+
}
|
|
78
85
|
if (rewrites === undefined || rewrites.length === 0 || run.journal !== undefined) return undefined
|
|
79
86
|
const descriptions = rewrites.map(rewrite => rewrite.description).join('; ')
|
|
80
87
|
const details = descriptions.length <= 2048 ? descriptions
|
|
@@ -100,7 +107,7 @@ export function sessionRuntimeContexts(agent, tipConfig, options = {}) {
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/** Project messages without treating an uncommitted proposal as delivery. */
|
|
103
|
-
export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
110
|
+
export function projectRuntimeMessages(view, contexts, pending = [], options) {
|
|
104
111
|
if (view.visibleRuntimeMessages === undefined) return []
|
|
105
112
|
const sections = contexts.filter(context => PTC_STATE_NAMES.includes(context.name) && context.name !== PTC_BINDING_CATALOG)
|
|
106
113
|
const catalog = contexts.find(context => context.name === PTC_BINDING_CATALOG)
|
|
@@ -114,7 +121,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
114
121
|
?? (pendingAggregate ?? legacy?.sections)?.filter(section => PTC_STATE_NAMES.includes(section.name))
|
|
115
122
|
const hadState = history.hadState
|
|
116
123
|
const messages = []
|
|
117
|
-
const proposed = pending.map(readRuntimeMessage).filter(record => record !== undefined)
|
|
124
|
+
const proposed = pending.map(record => readRuntimeMessage(record)).filter(record => record !== undefined)
|
|
118
125
|
const pendingState = proposed.filter(record => record.form === 'snapshot').at(-1)
|
|
119
126
|
const previous = pendingState?.sections ?? retained
|
|
120
127
|
if ((sections.length > 0 || (previous !== undefined && previous.length > 0) || (previous === undefined && hadState))
|
|
@@ -126,7 +133,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
126
133
|
const hadCatalog = history.hadCatalog
|
|
127
134
|
if ((catalog !== undefined || previousCatalog !== undefined || hadCatalog)
|
|
128
135
|
&& JSON.stringify(previousCatalog?.sections) !== JSON.stringify(catalog === undefined ? [] : [catalog])) {
|
|
129
|
-
messages.push(runtimeBindingCatalogMessage(catalog))
|
|
136
|
+
messages.push(runtimeBindingCatalogMessage(catalog, options))
|
|
130
137
|
}
|
|
131
138
|
for (const tip of contexts.filter(context => context.name.startsWith('tools:ptc-plus-tip/'))) {
|
|
132
139
|
if (proposed.some(record => record.form === 'notice' && record.name === tip.name)) continue
|
|
@@ -137,7 +144,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/** Passive message delivery follows the actual public assembly and accepted step. */
|
|
140
|
-
export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = projectSessionLog, activeForRequest, sessionQuery) {
|
|
147
|
+
export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = projectSessionLog, activeForRequest, sessionQuery, promptForRequest) {
|
|
141
148
|
const requests = new WeakMap()
|
|
142
149
|
const disposedAgents = new WeakSet()
|
|
143
150
|
let disposed = false
|
|
@@ -162,7 +169,9 @@ export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = p
|
|
|
162
169
|
if (disposed || decision.kind !== 'enter' || payload.signal?.aborted === true
|
|
163
170
|
|| request?.allowed !== true || request.context.signal !== payload.signal) return decision
|
|
164
171
|
const view = sessionLogView(payload.agent)
|
|
165
|
-
const
|
|
172
|
+
const resolved = promptForRequest === undefined ? undefined : promptForRequest(request.context)
|
|
173
|
+
const messages = projectRuntimeMessages(view, contextsForRequest(request.context), decision.messages,
|
|
174
|
+
resolved === undefined ? undefined : { prompt: resolved })
|
|
166
175
|
if (activeForRequest !== undefined) {
|
|
167
176
|
const log = await readSessionLog(payload.agent.session, sessionQuery, payload.signal)
|
|
168
177
|
const availability = projectRuntimeAvailability([
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import {
|
|
3
|
+
CATALOG_PREFIX, EMPTY_CATALOG, LEGACY_STATE_PREFIX, PREVIOUS_EMPTY_CATALOG, PREVIOUS_STATE_PREFIX,
|
|
4
|
+
PROMPT_SLOT_IDS, PROMPT_TEXT_DEFAULT, STATE_PREFIX, resolvePromptText,
|
|
5
|
+
} from './prompt-text.js'
|
|
2
6
|
|
|
3
7
|
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
4
8
|
|
|
@@ -13,16 +17,12 @@ export const PTC_STATE_NAMES = Object.freeze([
|
|
|
13
17
|
])
|
|
14
18
|
const MAX_STATE_CODE_UNITS = 65536
|
|
15
19
|
const MAX_NOTICE_CODE_UNITS = 8192
|
|
16
|
-
|
|
17
|
-
const PREVIOUS_STATE_PREFIX = 'PTC Plus runtime recovery state. This replaces only earlier PTC state snapshots and PTC sections in historical aggregate snapshots; it does not replace the global binding API catalog, tasks, Skill instructions, tool results, or other producers.'
|
|
20
|
+
|
|
18
21
|
const PREVIOUS_CATALOG_PREFIXES = [
|
|
19
22
|
'Global binding API catalog (PTC Plus). Only a later global binding API catalog replaces this catalog. Host runtime-context snapshots and PTC recovery snapshots do not withdraw it. This describes configured APIs, not successful initialization or current runtime values.',
|
|
20
23
|
'Global binding API catalog (PTC Plus). Replaces earlier binding catalogs only; remains applicable until the next binding catalog.',
|
|
21
24
|
]
|
|
22
|
-
|
|
23
|
-
const CATALOG_PREFIX = 'Global binding API reference for run_code (replaces the previous global binding reference):'
|
|
24
|
-
const EMPTY_CATALOG = 'No entries documented.'
|
|
25
|
-
const PREVIOUS_EMPTY_CATALOG = 'No global binding API documentation is currently configured. Earlier binding catalogs no longer apply.'
|
|
25
|
+
|
|
26
26
|
const AVAILABILITY_TEXT = Object.freeze({
|
|
27
27
|
active: 'PTC Plus is the current continuous-computation provider. Follow its current tool schemas and capability plan; earlier withdrawals do not restore old computation state.',
|
|
28
28
|
disabled: 'PTC Plus is disabled. Earlier PTC Plus continuous-computation instructions, capability plans, discovery instructions, calling-contract disclosures and global binding references no longer describe the current runtime. Use the current Host tools and contracts. Historical computations and effects remain historical; no old PTC Plus heap has been restored. Tasks, Skill instructions, other producers and tool results are unchanged.',
|
|
@@ -56,10 +56,13 @@ export function projectRuntimeAvailability(messages, active, computationDeclared
|
|
|
56
56
|
const last = messages.map(readRuntimeAvailability).filter(Boolean).at(-1)
|
|
57
57
|
if (active) return last && last !== 'active' ? runtimeAvailabilityMessage('active') : undefined
|
|
58
58
|
if (last === 'disabled' || last === 'unloaded') return undefined
|
|
59
|
-
const declared = computationDeclared || last === 'active' || messages.some(message =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const declared = computationDeclared || last === 'active' || messages.some(message => {
|
|
60
|
+
const recognized = isPtcMessageSource(message?.source) ? readRuntimeMessage(message) : undefined
|
|
61
|
+
return isPtcMessageSource(message?.source)
|
|
62
|
+
&& (['capability-navigation', 'capability-descriptions', 'capability-contracts'].includes(message.source.form)
|
|
63
|
+
|| message.source.form === 'capability-plan' && message.source.snapshot?.plan?.enabled === true
|
|
64
|
+
|| recognized?.form === 'catalog' && recognized.sections.length > 0)
|
|
65
|
+
})
|
|
63
66
|
return declared ? runtimeAvailabilityMessage('disabled') : undefined
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -92,6 +95,18 @@ function stateText(sections, prefix = STATE_PREFIX) {
|
|
|
92
95
|
: sections.map(section => section.text).join('\n\n')}`
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Historical emptiness wording a recorded catalog may carry.
|
|
100
|
+
*
|
|
101
|
+
* These are code-owned constants, not the resolved document: a session record
|
|
102
|
+
* written by an older build must stay readable after today's sentence changes,
|
|
103
|
+
* and recognizing it must never depend on the user's current override.
|
|
104
|
+
*/
|
|
105
|
+
const HISTORICAL_EMPTY_CATALOGS = Object.freeze([EMPTY_CATALOG, PREVIOUS_EMPTY_CATALOG])
|
|
106
|
+
|
|
107
|
+
/** The resolved document a producer used. Recognition never depends on its text. */
|
|
108
|
+
const promptOf = prompt => prompt ?? resolvePromptText(PROMPT_TEXT_DEFAULT)
|
|
109
|
+
|
|
95
110
|
/** Recognize only bounded, formed PTC records, never authoring tasks or Skills. */
|
|
96
111
|
export function readRuntimeMessage(message) {
|
|
97
112
|
const source = message?.source
|
|
@@ -112,7 +127,12 @@ export function readRuntimeMessage(message) {
|
|
|
112
127
|
if (prefix === undefined) return undefined
|
|
113
128
|
const body = text.slice(prefix.length + 2)
|
|
114
129
|
if (body.length > 0 && body.length <= MAX_STATE_CODE_UNITS) {
|
|
115
|
-
|
|
130
|
+
// Emptiness is a property of the record (`source.empty`), so live delivery
|
|
131
|
+
// and log replay agree even when the empty sentence is customized. Records
|
|
132
|
+
// written before that flag existed are matched against the code-owned
|
|
133
|
+
// historical wording, never against the current resolved text.
|
|
134
|
+
const empty = source.empty === true
|
|
135
|
+
|| (source.empty === undefined && HISTORICAL_EMPTY_CATALOGS.includes(body))
|
|
116
136
|
return { form: 'catalog', sections: stateSections(empty ? [] : [{ name: PTC_BINDING_CATALOG, text: body }]) }
|
|
117
137
|
}
|
|
118
138
|
} else if (source.form === 'notice' && recoveryTipIdentity(source.summary) !== undefined
|
|
@@ -123,15 +143,23 @@ export function readRuntimeMessage(message) {
|
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
/** One literal configuration document, independent of runtime recovery state. */
|
|
126
|
-
export function runtimeBindingCatalogMessage(context) {
|
|
146
|
+
export function runtimeBindingCatalogMessage(context, options) {
|
|
147
|
+
const resolved = promptOf(options?.prompt)
|
|
148
|
+
const catalogPrefix = resolved.text(PROMPT_SLOT_IDS.catalogPrefix)
|
|
149
|
+
const emptyCatalog = resolved.text(PROMPT_SLOT_IDS.catalogEmpty)
|
|
150
|
+
// Emptiness travels in `source.empty`, so a catalog body may legitimately equal
|
|
151
|
+
// the code-owned empty wording; comparing the text would abort the whole step for
|
|
152
|
+
// a deliverable catalog. Only the shape is rejected here.
|
|
127
153
|
if (context !== undefined && (context?.name !== PTC_BINDING_CATALOG
|
|
128
154
|
|| typeof context.text !== 'string' || context.text.length === 0
|
|
129
|
-
|| context.text.length > MAX_STATE_CODE_UNITS
|
|
155
|
+
|| context.text.length > MAX_STATE_CODE_UNITS)) {
|
|
130
156
|
throw new Error('invalid PTC binding catalog')
|
|
131
157
|
}
|
|
132
158
|
return createUserMessage({
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
// `empty` records the fact itself, so replay recognizes an empty catalog from
|
|
160
|
+
// the record instead of comparing it against today's sentence.
|
|
161
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'catalog', empty: context === undefined },
|
|
162
|
+
content: [{ type: 'text', text: `${catalogPrefix}\n\n${context?.text ?? emptyCatalog}` }],
|
|
135
163
|
})
|
|
136
164
|
}
|
|
137
165
|
|
|
@@ -26,9 +26,11 @@ import {
|
|
|
26
26
|
import { assertStateName, LIVE_USER_BINDINGS_REUSE_POLICY } from './session-journal.js'
|
|
27
27
|
import { LIVE_USER_BINDINGS_SHADOW_POLICY, normalizeUserBindingNames } from './session-journal-schema.js'
|
|
28
28
|
import { PreflightError, prepareProgram } from './compiler-service.js'
|
|
29
|
+
import { UNANSWERED_SAMPLE_CODE } from './isolated-worker.js'
|
|
29
30
|
import { executionPolicies } from './binding-update-policy.js'
|
|
31
|
+
import { resultEnvelopeText, resolvePromptTextConfig, rewriteDescriptions } from './prompt-text.js'
|
|
30
32
|
import { createReplMemorySnapshot } from './repl-memory-projection.js'
|
|
31
|
-
import { ModuleRewriteError } from './cell-error.js'
|
|
33
|
+
import { CellAdmissionBlocked, ModuleRewriteError } from './cell-error.js'
|
|
32
34
|
import { mapSourcePosition } from './source-position-map.js'
|
|
33
35
|
import { durabilityState, transitionDurability } from './session-state.js'
|
|
34
36
|
import { dynamicBindingOrigin } from './dynamic-binding-evidence.js'
|
|
@@ -38,7 +40,6 @@ import {
|
|
|
38
40
|
selectUserBindingsSnapshot,
|
|
39
41
|
} from './user-bindings.js'
|
|
40
42
|
|
|
41
|
-
const OUTPUT_LIMIT_MESSAGE = bytes => `output exceeded ${bytes} bytes; this cell was discarded and the worker reset. Only bindings in the verified recovery frontier can be restored. External effects may already have occurred and are not undone. For future cells, bound console output and return a smaller value; do not assume this cell's values remain available.`
|
|
42
43
|
|
|
43
44
|
function earlyResult(kind, message) {
|
|
44
45
|
return { logs: [], error: { kind, message } }
|
|
@@ -94,7 +95,7 @@ function parseDiagnostic(error, source, position, repair) {
|
|
|
94
95
|
: ['this cell was not executed; correct the reported syntax and retry only this cell with run_code']
|
|
95
96
|
: [
|
|
96
97
|
`this cell was not executed; validated syntax repair: append ${JSON.stringify(repair.delimiter)} at the end of this cell`,
|
|
97
|
-
`when this correction matches your intent
|
|
98
|
+
`when this correction matches your intent, tools.edit_run_code can apply it to call_seq ${repair.targetCallSeq} (same anchor with ${JSON.stringify(repair.delimiter)} appended); the accepted edit reruns the corrected cell after this one, otherwise submit corrected source in run_code`,
|
|
98
99
|
],
|
|
99
100
|
})
|
|
100
101
|
}
|
|
@@ -366,6 +367,35 @@ function stateArguments(value) {
|
|
|
366
367
|
export class SessionCellExecutor {
|
|
367
368
|
constructor(kernel) {
|
|
368
369
|
this.kernel = kernel
|
|
370
|
+
this.pendingRestore = undefined
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Install recorded values into the worker without running any program.
|
|
375
|
+
*
|
|
376
|
+
* Recovery restores a cell the journal recorded as volatile from the values
|
|
377
|
+
* that cell produced. Sending its source instead would redispatch whatever
|
|
378
|
+
* ambient source made it volatile, so this path carries values only: the
|
|
379
|
+
* worker commits each one as a declaration and reports the names it could
|
|
380
|
+
* not install. A missing worker is reported the same way rather than thrown,
|
|
381
|
+
* because the caller has to tell the model which bindings it lost.
|
|
382
|
+
*/
|
|
383
|
+
async restoreBindings(values, config = this.kernel.config) {
|
|
384
|
+
const kernel = this.kernel
|
|
385
|
+
const failedAll = reason => values.map(entry => ({ name: entry.name, reason }))
|
|
386
|
+
if (kernel.disposed) return { restored: [], failed: failedAll('session kernel disposed') }
|
|
387
|
+
if (kernel.client.worker === undefined) return { restored: [], failed: failedAll('no live worker') }
|
|
388
|
+
const id = ++kernel.sequence
|
|
389
|
+
const valueLimits = kernel.valueLimits(config)
|
|
390
|
+
return new Promise(resolve => {
|
|
391
|
+
this.pendingRestore = { id, resolve }
|
|
392
|
+
try {
|
|
393
|
+
kernel.client.post({ type: 'restore', id, values, valueLimits })
|
|
394
|
+
} catch (error) {
|
|
395
|
+
this.pendingRestore = undefined
|
|
396
|
+
resolve({ restored: [], failed: failedAll(error instanceof Error ? error.message : String(error)) })
|
|
397
|
+
}
|
|
398
|
+
})
|
|
369
399
|
}
|
|
370
400
|
|
|
371
401
|
async executeCell(request, replayRecord = undefined, config = this.kernel.config) {
|
|
@@ -380,6 +410,24 @@ export class SessionCellExecutor {
|
|
|
380
410
|
kernel.completeJournal(request.journal, 'noop', result)
|
|
381
411
|
return result
|
|
382
412
|
}
|
|
413
|
+
// A restore-only request installs recorded values: it must not compile or run
|
|
414
|
+
// the node's program. Preparing it first could reject source that is dead by
|
|
415
|
+
// definition — the node is not on the path to the head, so it may reference
|
|
416
|
+
// bindings a recorded rewind ended — and a rejected preparation is what made
|
|
417
|
+
// the off-path repair install nothing. The catalog advances from the recorded
|
|
418
|
+
// names instead, so later cells still compile against them.
|
|
419
|
+
if (request.restoreValues !== undefined) {
|
|
420
|
+
try {
|
|
421
|
+
await kernel.prepareWorker()
|
|
422
|
+
await kernel.ensureWorker(config.maxOldGenerationSizeMb)
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error instanceof CellAdmissionBlocked) throw error
|
|
425
|
+
return earlyResult('worker-exit', messageOf(error))
|
|
426
|
+
}
|
|
427
|
+
const outcome = await this.restoreBindings(request.restoreValues, config)
|
|
428
|
+
kernel.bindingCatalog = kernel.bindingCatalog.restore(outcome.restored)
|
|
429
|
+
return { logs: [], restored: outcome.restored, failed: outcome.failed }
|
|
430
|
+
}
|
|
383
431
|
|
|
384
432
|
let userBindings
|
|
385
433
|
const userBindingsShadowPolicy = replayRecord?.userBindingsShadowPolicy ?? LIVE_USER_BINDINGS_SHADOW_POLICY
|
|
@@ -425,6 +473,7 @@ export class SessionCellExecutor {
|
|
|
425
473
|
const moduleDirectory = executionCwd ?? process.cwd()
|
|
426
474
|
const moduleFilename = resolve(moduleDirectory, 'repl')
|
|
427
475
|
const prepareCell = program => prepareProgram(program, {
|
|
476
|
+
rewriteDescriptions: rewriteDescriptions(resolvePromptTextConfig(config)),
|
|
428
477
|
knownBindings: catalog.knownBindings,
|
|
429
478
|
bindingPolicy,
|
|
430
479
|
languageSemantics,
|
|
@@ -483,8 +532,12 @@ export class SessionCellExecutor {
|
|
|
483
532
|
let worker
|
|
484
533
|
try {
|
|
485
534
|
await kernel.prepareWorker()
|
|
486
|
-
worker = await kernel.
|
|
535
|
+
worker = await kernel.ensureWorker(config.maxOldGenerationSizeMb)
|
|
487
536
|
} catch (error) {
|
|
537
|
+
// An admission block is the kernel's own lifecycle fact, so the kernel
|
|
538
|
+
// settles the cell: it must not be read as a failure of recorded history
|
|
539
|
+
// or of this source.
|
|
540
|
+
if (error instanceof CellAdmissionBlocked) throw error
|
|
488
541
|
const result = earlyResult('worker-exit', messageOf(error))
|
|
489
542
|
kernel.completeJournal(request.journal, 'discarded', result)
|
|
490
543
|
kernel.rollbackToDurable()
|
|
@@ -510,6 +563,7 @@ export class SessionCellExecutor {
|
|
|
510
563
|
const active = {
|
|
511
564
|
id,
|
|
512
565
|
started: false,
|
|
566
|
+
dispatched: false,
|
|
513
567
|
request: { ...request, bindings: bindings.namespaces },
|
|
514
568
|
finish: resolve,
|
|
515
569
|
computeTimer: undefined,
|
|
@@ -561,23 +615,57 @@ export class SessionCellExecutor {
|
|
|
561
615
|
// cache. Work that completed before this request must not be charged to
|
|
562
616
|
// the new cell, and work after the sample must remain counted.
|
|
563
617
|
active.wallTimer = setTimeout(() => {
|
|
618
|
+
// A cell that never reached the runtime has executed nothing, so this
|
|
619
|
+
// failure cannot discard an earlier settlement or justify rebuilding the
|
|
620
|
+
// worker on its own; SessionRuntime owns how many such cells it keeps.
|
|
621
|
+
if (active.dispatched !== true) {
|
|
622
|
+
kernel.abandonCell(active, earlyResult(
|
|
623
|
+
'budget-unavailable',
|
|
624
|
+
`wall-clock ceiling reached (${config.maxWallMs}ms) before this cell was dispatched; no code from it ran; retry the cell or raise maxWallMs`,
|
|
625
|
+
))
|
|
626
|
+
return
|
|
627
|
+
}
|
|
564
628
|
active.resolve(earlyResult('timeout', `wall-clock ceiling reached (${config.maxWallMs}ms); split long-running work into smaller cells`), true)
|
|
565
629
|
}, config.maxWallMs)
|
|
566
630
|
active.budgetsReady = (async () => {
|
|
567
631
|
let started
|
|
568
632
|
try {
|
|
633
|
+
// How long this environment may wait for an answer belongs to the
|
|
634
|
+
// cell's configuration; the transport reads its bound per request.
|
|
635
|
+
if (Number.isSafeInteger(config.sampleBoundMs) && config.sampleBoundMs > 0) {
|
|
636
|
+
worker.sampleBoundMs = config.sampleBoundMs
|
|
637
|
+
}
|
|
569
638
|
started = await worker.sampleUtilization()
|
|
570
639
|
} catch (error) {
|
|
571
|
-
// No fresh baseline means this request cannot be measured
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
640
|
+
// No fresh baseline means this request cannot be measured: the
|
|
641
|
+
// helper's periodic cache may predate work that already finished, so
|
|
642
|
+
// it is not a substitute start point. A helper that is still trusted
|
|
643
|
+
// has not failed, and no frame was dispatched yet, so the unmeasured
|
|
644
|
+
// cell settles without execution. Only a helper the transport reports
|
|
645
|
+
// as lost takes the worker-exit route.
|
|
646
|
+
if (safeProperty(error, 'code') === UNANSWERED_SAMPLE_CODE) {
|
|
647
|
+
kernel.abandonCell(active, earlyResult(
|
|
648
|
+
'budget-unavailable',
|
|
649
|
+
`${messageOf(error)}; this cell was not dispatched and no code from it ran; retry the cell`,
|
|
650
|
+
))
|
|
651
|
+
} else {
|
|
652
|
+
active.resolve(earlyResult('worker-exit', messageOf(error)), true)
|
|
653
|
+
}
|
|
575
654
|
return
|
|
576
655
|
}
|
|
577
656
|
if (active.settled) return
|
|
578
657
|
active.computeTimer = setInterval(() => {
|
|
579
658
|
if (active.settled) return
|
|
580
659
|
if (worker.performance.eventLoopUtilization(started).active > config.computeMs) {
|
|
660
|
+
// Nothing from this cell was dispatched, so an exhausted budget
|
|
661
|
+
// measures earlier work on this worker, not this cell's source.
|
|
662
|
+
if (active.dispatched !== true) {
|
|
663
|
+
kernel.abandonCell(active, earlyResult(
|
|
664
|
+
'budget-unavailable',
|
|
665
|
+
`compute budget exhausted (${config.computeMs}ms event-loop active) before this cell was dispatched; no code from this cell ran; a callback from earlier work may still be blocking the worker; retry the cell or restart the session`,
|
|
666
|
+
))
|
|
667
|
+
return
|
|
668
|
+
}
|
|
581
669
|
active.resolve(earlyResult('timeout', `compute budget exhausted (${config.computeMs}ms event-loop active, including synchronous blocking); this measure does not establish CPU use or the specific cause. Use asynchronous waiting for long waits, or a DSH-owned managed job if available. This cell was discarded and the worker reset; external effects or processes may continue.`), true)
|
|
582
670
|
}
|
|
583
671
|
}, Math.min(100, config.computeMs))
|
|
@@ -604,6 +692,12 @@ export class SessionCellExecutor {
|
|
|
604
692
|
returnSignal: prepared.returnSignal,
|
|
605
693
|
asyncCompletion: prepared.asyncCompletion,
|
|
606
694
|
commitSignal: prepared.commitSignal,
|
|
695
|
+
// Compile-time evidence that a binding's initializer is a bounded
|
|
696
|
+
// object literal. Nothing records from it yet; settlement will use
|
|
697
|
+
// it to decide whether a value can be captured without walking it.
|
|
698
|
+
declarationEvidence: prepared.declarations
|
|
699
|
+
.filter(declaration => declaration.boundedLiteral !== undefined)
|
|
700
|
+
.map(declaration => [declaration.name, declaration.boundedLiteral]),
|
|
607
701
|
maxOutputBytes: config.maxOutputBytes,
|
|
608
702
|
valueLimits,
|
|
609
703
|
durability,
|
|
@@ -624,6 +718,9 @@ export class SessionCellExecutor {
|
|
|
624
718
|
...request.bindingDescriptors.reservedNames,
|
|
625
719
|
])],
|
|
626
720
|
})
|
|
721
|
+
// The run frame is with the worker now, so a later failure may have
|
|
722
|
+
// executed this cell's source and cannot settle as an unmeasured cell.
|
|
723
|
+
active.dispatched = true
|
|
627
724
|
} catch (error) {
|
|
628
725
|
active.resolve(earlyResult('worker-exit', messageOf(error)), true)
|
|
629
726
|
}
|
|
@@ -689,7 +786,7 @@ export class SessionCellExecutor {
|
|
|
689
786
|
throw new Error(`cannot save a durable REPL state from a volatile segment; volatile reason: ${active.durability.reason ?? 'replayability is unproved'}. This save rejection leaves live bindings usable in the current worker; continue computation from them. It does not undo external effects.`)
|
|
690
787
|
}
|
|
691
788
|
active.control.names.add(name)
|
|
692
|
-
active.journal.operations.push({ action, name })
|
|
789
|
+
active.journal.operations.push({ action, name, kind: 'checkpoint' })
|
|
693
790
|
return { action, name, saved: true }
|
|
694
791
|
}
|
|
695
792
|
if (action === 'delete') {
|
|
@@ -703,6 +800,18 @@ export class SessionCellExecutor {
|
|
|
703
800
|
}
|
|
704
801
|
|
|
705
802
|
onMessage(message) {
|
|
803
|
+
if (message?.type === 'restored') {
|
|
804
|
+
const pending = this.pendingRestore
|
|
805
|
+
if (pending === undefined || pending.id !== message.id) return
|
|
806
|
+
this.pendingRestore = undefined
|
|
807
|
+
const restored = Array.isArray(message.restored) ? message.restored.filter(name => typeof name === 'string') : []
|
|
808
|
+
const failed = Array.isArray(message.failed)
|
|
809
|
+
? message.failed.filter(entry => entry !== null && typeof entry === 'object' && typeof entry.name === 'string')
|
|
810
|
+
.map(entry => ({ name: entry.name, reason: typeof entry.reason === 'string' ? entry.reason : 'the worker did not report a reason' }))
|
|
811
|
+
: []
|
|
812
|
+
pending.resolve({ restored, failed })
|
|
813
|
+
return
|
|
814
|
+
}
|
|
706
815
|
if (message?.type === 'observation-started') {
|
|
707
816
|
const kernel = this.kernel
|
|
708
817
|
const observation = kernel.workerObservation
|
|
@@ -759,7 +868,7 @@ export class SessionCellExecutor {
|
|
|
759
868
|
const logs = Array.isArray(message.logs) && message.logs.every(log => typeof log === 'string') ? message.logs : []
|
|
760
869
|
active.resolve({
|
|
761
870
|
logs: limitLogs(logs),
|
|
762
|
-
error: { kind: 'output-limit', message:
|
|
871
|
+
error: { kind: 'output-limit', message: resultEnvelopeText(resolvePromptTextConfig(active.config), active.config.maxOutputBytes) },
|
|
763
872
|
}, true)
|
|
764
873
|
}
|
|
765
874
|
|
|
@@ -791,6 +900,18 @@ export class SessionCellExecutor {
|
|
|
791
900
|
reason: typeof message.volatileReason === 'string' ? message.volatileReason : undefined,
|
|
792
901
|
})
|
|
793
902
|
}
|
|
903
|
+
// The worker sends the values this settlement produced, durable or volatile, so
|
|
904
|
+
// the journal can keep them; recovery installs them instead of re-running the
|
|
905
|
+
// cell when its replay path does not reach it. A value the worker could not
|
|
906
|
+
// transfer arrives in `unrecorded` and is named in the record.
|
|
907
|
+
const recorded = message.bindingValues ?? message.volatileBindingValues
|
|
908
|
+
if (recorded !== undefined && typeof recorded === 'object'
|
|
909
|
+
&& Array.isArray(recorded.values) && Array.isArray(recorded.unrecorded)
|
|
910
|
+
&& recorded.values.every(entry => entry !== null && typeof entry === 'object'
|
|
911
|
+
&& typeof entry.name === 'string' && entry.wire !== undefined)
|
|
912
|
+
&& recorded.unrecorded.every(name => typeof name === 'string')) {
|
|
913
|
+
active.bindingValues = recorded
|
|
914
|
+
}
|
|
794
915
|
if (active.replay !== undefined && active.durability.status !== 'durable') {
|
|
795
916
|
active.resolve({ logs, error: { kind: 'recovery', message: 'durable history requested a volatile capability during replay' } }, true)
|
|
796
917
|
return
|
|
@@ -851,7 +972,7 @@ export class SessionCellExecutor {
|
|
|
851
972
|
if (bytes > active.config.maxOutputBytes) {
|
|
852
973
|
active.resolve({
|
|
853
974
|
logs: limitLogs(logs),
|
|
854
|
-
error: { kind: 'output-limit', message:
|
|
975
|
+
error: { kind: 'output-limit', message: resultEnvelopeText(resolvePromptTextConfig(active.config), active.config.maxOutputBytes) },
|
|
855
976
|
}, true)
|
|
856
977
|
return
|
|
857
978
|
}
|