dsh-ptc-plus 0.5.0 → 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 +15 -3
- package/README.md +15 -3
- package/client.cjs +1983 -234
- 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/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- 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/0038-freeze-session-capability-plans.md +1 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- 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/adr/README.md +2 -0
- package/docs/architecture.md +2 -2
- package/docs/capability-planning-components.md +2 -2
- package/docs/client-ui.md +13 -10
- package/docs/client-visual-contract.md +1 -1
- package/docs/durability-design.md +7 -1
- package/docs/installation.md +5 -3
- package/docs/publishing.md +2 -2
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +42 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +63 -19
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +3 -1
- package/internal/capability-environment.js +171 -13
- package/internal/capability-plan.js +11 -8
- package/internal/capability-statistics.js +6 -2
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +122 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +96 -17
- package/internal/execution-provider-owner.js +25 -0
- package/internal/execution-seam-compat.js +18 -0
- 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/official-repl-priority.js +51 -0
- 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 +11 -8
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/sdk-projection.js +2 -2
- 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 +8 -27
- 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 +5 -2
- package/src/client-activity.js +27 -6
- package/src/client-capabilities-view.js +774 -125
- package/src/client-copy.js +231 -18
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +68 -2
- package/src/client-tool-view.js +9 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retention window for the host-only session-log projection.
|
|
5
|
+
*
|
|
6
|
+
* The projection is a fold shortcut, not the session log. Keeping the payload of
|
|
7
|
+
* every call would make a checkpoint proportional to how many bytes the session
|
|
8
|
+
* wrote (issue #10: a 77 MB state that the host cloned and pretty-printed until
|
|
9
|
+
* it aborted). The window is therefore bounded by default: the newest clusters
|
|
10
|
+
* keep their argument text so tips and the current edit target stay synchronous,
|
|
11
|
+
* and every older cluster keeps its identity, its observations and its call
|
|
12
|
+
* record while its payload text becomes a digest. Only a cluster beyond
|
|
13
|
+
* `retentionClusters` is folded into the chained summary, and a cluster whose
|
|
14
|
+
* payload alone exceeds the byte budget keeps its identity with a digested
|
|
15
|
+
* payload — the bound wins over the convenience of the hot cell, and the edit
|
|
16
|
+
* path refolds that cell from the session log.
|
|
17
|
+
*
|
|
18
|
+
* `retentionPayloadBytes` bounds the retained payload (the settlement and
|
|
19
|
+
* executable copies). A checkpoint additionally carries the selected run's
|
|
20
|
+
* copies, so its serialized total can approach roughly twice the budget plus the
|
|
21
|
+
* per-cluster metadata; that is the shipped bound, stated in ADR 0032.
|
|
22
|
+
*
|
|
23
|
+
* The environment overrides may only *lower* the window for tests; a value that
|
|
24
|
+
* would disable the bound is ignored so a deployment cannot silently return to
|
|
25
|
+
* an unbounded checkpoint.
|
|
26
|
+
*/
|
|
27
|
+
export const RETENTION_BOUNDS = Object.freeze({
|
|
28
|
+
runs: Object.freeze({ min: 8, max: 512 }),
|
|
29
|
+
bytes: Object.freeze({ min: 1024 * 1024, max: 16 * 1024 * 1024 }),
|
|
30
|
+
// 0 is the only unlimited spelling; a cluster cap of 0 means the count window
|
|
31
|
+
// never folds anything and only the payload budget bounds the checkpoint.
|
|
32
|
+
clusters: Object.freeze({ min: 0, max: 50000 }),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_RETENTION_LIMITS = Object.freeze({
|
|
36
|
+
runs: 8,
|
|
37
|
+
bytes: 8 * 1024 * 1024,
|
|
38
|
+
// The count window defaults to the payload window: cells beyond it fold into the
|
|
39
|
+
// chained summary instead of keeping a per-cluster record, which is what keeps a
|
|
40
|
+
// long session from growing linearly. A larger value keeps more recovery evidence
|
|
41
|
+
// without a log refold at the cost of a bigger checkpoint.
|
|
42
|
+
clusters: 8,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The environment levers exist for tests and for a deployment that wants a
|
|
47
|
+
* *smaller* window. They fill a field only when configuration leaves it absent
|
|
48
|
+
* and are clamped to the configured or default value, so no environment can
|
|
49
|
+
* widen the bound beyond what the settings say.
|
|
50
|
+
*/
|
|
51
|
+
const ENVIRONMENT_KEYS = Object.freeze({
|
|
52
|
+
runs: 'DSH_PTC_RETENTION_RUNS',
|
|
53
|
+
bytes: 'DSH_PTC_RETENTION_BYTES',
|
|
54
|
+
clusters: 'DSH_PTC_RETENTION_CLUSTERS',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function normalizeLimit(kind, raw, fallback) {
|
|
58
|
+
if (raw === undefined || raw === null || raw === '') return fallback
|
|
59
|
+
const value = Number(raw)
|
|
60
|
+
const { min, max } = RETENTION_BOUNDS[kind]
|
|
61
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < min || value > max) return fallback
|
|
62
|
+
// 0 means unlimited for clusters; every other value is clamped to the fallback
|
|
63
|
+
// so an environment lever can never widen the window past the configuration.
|
|
64
|
+
return kind === 'clusters' && value === 0 ? 0 : Math.min(value, fallback)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A configured window is honoured as written: it is the user's setting, bounded by
|
|
69
|
+
* `RETENTION_BOUNDS` only. The clamp in `normalizeLimit` exists for the environment
|
|
70
|
+
* lever, which may tighten a configured window but never widen it.
|
|
71
|
+
*/
|
|
72
|
+
function normalizeConfigured(kind, raw) {
|
|
73
|
+
if (raw === undefined || raw === null || raw === '') return DEFAULT_RETENTION_LIMITS[kind]
|
|
74
|
+
const value = Number(raw)
|
|
75
|
+
const { min, max } = RETENTION_BOUNDS[kind]
|
|
76
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < min || value > max) {
|
|
77
|
+
return DEFAULT_RETENTION_LIMITS[kind]
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let activeLimits = resolveRetentionLimits({})
|
|
83
|
+
|
|
84
|
+
/** The limits the current fold prunes against. */
|
|
85
|
+
export function retentionLimits() {
|
|
86
|
+
return activeLimits
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the effective limits from plugin configuration: configuration wins,
|
|
91
|
+
* the environment fills an absent field, and anything invalid falls back to the
|
|
92
|
+
* default rather than to an unbounded checkpoint.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveRetentionLimits(config = {}) {
|
|
95
|
+
const configured = {
|
|
96
|
+
runs: config.retentionPayloadRuns,
|
|
97
|
+
bytes: config.retentionPayloadBytes,
|
|
98
|
+
clusters: config.retentionClusters,
|
|
99
|
+
}
|
|
100
|
+
const resolved = {}
|
|
101
|
+
// The cluster window follows the payload window unless it is configured: a user
|
|
102
|
+
// who raises the retained cells expects the same number of cells to keep their
|
|
103
|
+
// cluster records, not eight of them.
|
|
104
|
+
if (configured.clusters === undefined || configured.clusters === null || configured.clusters === '') {
|
|
105
|
+
configured.clusters = normalizeConfigured('runs', configured.runs)
|
|
106
|
+
}
|
|
107
|
+
for (const kind of ['runs', 'bytes', 'clusters']) {
|
|
108
|
+
const base = normalizeConfigured(kind, configured[kind])
|
|
109
|
+
// The environment may only tighten the configured bound, never widen it.
|
|
110
|
+
resolved[kind] = normalizeLimit(kind, process.env[ENVIRONMENT_KEYS[kind]], base)
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze(resolved)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Install the limits for the folds that follow; returns the previous ones. */
|
|
116
|
+
export function configureRetention(config = {}) {
|
|
117
|
+
const previous = activeLimits
|
|
118
|
+
activeLimits = resolveRetentionLimits(config)
|
|
119
|
+
return previous
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The count of clusters kept when the configuration folds by count. */
|
|
123
|
+
export function retainedClusterCount(limits = activeLimits) {
|
|
124
|
+
if (limits.clusters === 0) return Number.POSITIVE_INFINITY
|
|
125
|
+
return Math.min(limits.runs, limits.clusters)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Shout when the retention window cannot hold the payload a consumer needs. */
|
|
129
|
+
export const PRUNED_EDIT = '该单元格超出保留窗口,已不可编辑'
|
|
130
|
+
export const PRUNED_EDIT_EN = 'This cell is outside the retention window and can no longer be edited'
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The checkpoint's summary and the session log disagree about a dropped cluster.
|
|
134
|
+
* Neither side may be used: the edit must be refused rather than applied to a cell
|
|
135
|
+
* the log and the checkpoint describe differently.
|
|
136
|
+
*/
|
|
137
|
+
export const PRUNED_SUMMARY_MISMATCH = '保留摘要与被重折的结算不一致'
|
|
138
|
+
export const PRUNED_SUMMARY_MISMATCH_EN = 'Retention summary disagrees with the refolded settlement'
|
|
139
|
+
|
|
140
|
+
/** The newest cell alone exceeded the byte budget and was dropped from the checkpoint. */
|
|
141
|
+
export const HOT_CELL_DROPPED = '最新单元格超出保留预算,已从检查点移除'
|
|
142
|
+
export const HOT_CELL_DROPPED_EN = 'The newest cell exceeded the retention budget and was removed from the checkpoint'
|
|
143
|
+
|
|
144
|
+
export function digestOf(value) {
|
|
145
|
+
return createHash('sha256').update(JSON.stringify(value ?? null, (_key, field) => (
|
|
146
|
+
isPlainRecord(field) ? Object.fromEntries(Object.keys(field).sort().map(key => [key, field[key]])) : field
|
|
147
|
+
))).digest('hex')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isPlainRecord(value) {
|
|
151
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function byteLength(value) {
|
|
155
|
+
if (typeof value === 'string') return Buffer.byteLength(value, 'utf8')
|
|
156
|
+
if (value === undefined || value === null) return 0
|
|
157
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Identity of one settlement for the retention summary.
|
|
162
|
+
*
|
|
163
|
+
* A dropped settlement keeps three facts: its result position, its call position
|
|
164
|
+
* and the digest of the argument text the log still holds. Those three are exactly
|
|
165
|
+
* what a fold of the session log can reproduce, so the chain over them can be
|
|
166
|
+
* recomputed from the log; the normalized digest is recorded for diagnostics but
|
|
167
|
+
* stays out of the chain, because a settlement that was compacted earlier recorded
|
|
168
|
+
* only its payload digest rather than the normalized object itself.
|
|
169
|
+
*/
|
|
170
|
+
export function summaryEntry(settlement, resultSeq) {
|
|
171
|
+
return {
|
|
172
|
+
resultSeq,
|
|
173
|
+
callSeq: settlement?.call?.seq,
|
|
174
|
+
argumentsDigest: settlement?.call?.data?.argumentsDigest ?? digestOf(settlement?.call?.data?.arguments ?? null),
|
|
175
|
+
normalizedDigest: settlement?.normalized?.payloadDigest ?? digestOf(settlement?.normalized ?? null),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The part of an entry the chain commits to: reproducible from the log alone. */
|
|
180
|
+
function chainEntry(entry) {
|
|
181
|
+
return { callSeq: entry.callSeq, resultSeq: entry.resultSeq, argumentsDigest: entry.argumentsDigest }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const EMPTY_CHAIN = digestOf(null)
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extend a chained retention summary with the settlements it just dropped.
|
|
188
|
+
*
|
|
189
|
+
* The summary keeps one compact record per dropped cluster rather than only a
|
|
190
|
+
* rolling digest. Admission receives no session log, so it cannot recompute the
|
|
191
|
+
* chain from the events; what it can do is prove the summary is internally
|
|
192
|
+
* consistent (count, ranges, chain over the stored records). The record itself is
|
|
193
|
+
* what makes the later guarantee possible: when a dropped settlement is refolded
|
|
194
|
+
* from the log, its digests are compared against the stored record and a mismatch
|
|
195
|
+
* is reported instead of being used.
|
|
196
|
+
*/
|
|
197
|
+
export function extendSummary(previous, entries) {
|
|
198
|
+
if (entries.length === 0) return previous
|
|
199
|
+
const records = [...(previous?.entries ?? [])]
|
|
200
|
+
let chain = previous?.chainDigest ?? EMPTY_CHAIN
|
|
201
|
+
let count = previous?.count ?? 0
|
|
202
|
+
let fromSeq = previous?.fromSeq
|
|
203
|
+
let toSeq = previous?.toSeq
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const record = chainEntry(entry)
|
|
206
|
+
records.push(record)
|
|
207
|
+
chain = digestOf({ chain, entry: record })
|
|
208
|
+
count += 1
|
|
209
|
+
fromSeq = fromSeq === undefined ? entry.resultSeq : Math.min(fromSeq, entry.resultSeq)
|
|
210
|
+
toSeq = toSeq === undefined ? entry.resultSeq : Math.max(toSeq, entry.resultSeq)
|
|
211
|
+
}
|
|
212
|
+
records.sort((left, right) => left.resultSeq - right.resultSeq)
|
|
213
|
+
return { fromSeq, toSeq, count, chainDigest: chain, entries: records }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Recompute the chain committed by a summary's own records. */
|
|
217
|
+
export function chainOfEntries(records) {
|
|
218
|
+
let chain = EMPTY_CHAIN
|
|
219
|
+
for (const record of records) chain = digestOf({ chain, entry: { callSeq: record.callSeq, resultSeq: record.resultSeq, argumentsDigest: record.argumentsDigest } })
|
|
220
|
+
return chain
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The record a dropped cluster must match when it is refolded from the log. */
|
|
224
|
+
export function summaryRecordOf(summary, resultSeq) {
|
|
225
|
+
if (!isPlainRecord(summary) || !Array.isArray(summary.entries)) return undefined
|
|
226
|
+
return summary.entries.find(record => record?.resultSeq === resultSeq)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Payload bytes a settlement still retains, used to decide pruning. */
|
|
230
|
+
export function settlementBytes(settlement) {
|
|
231
|
+
let bytes = 0
|
|
232
|
+
const args = settlement?.call?.data?.arguments
|
|
233
|
+
if (typeof args === 'string') bytes += byteLength(args)
|
|
234
|
+
const normalized = settlement?.normalized
|
|
235
|
+
if (normalized !== undefined) bytes += byteLength(normalized)
|
|
236
|
+
if (settlement?.publication !== undefined) bytes += byteLength(settlement.publication)
|
|
237
|
+
return bytes
|
|
238
|
+
}
|