dsh-ptc-plus 0.4.8 → 0.5.0

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.
Files changed (139) hide show
  1. package/README.en.md +61 -123
  2. package/README.md +61 -123
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42336 -0
  10. package/cordis.patch.yml +13 -2
  11. package/docs/README.md +7 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +3 -1
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
  23. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  24. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  25. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  26. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  27. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  28. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  29. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  30. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  31. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  32. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  33. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  34. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  35. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  36. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  37. package/docs/adr/README.md +6 -0
  38. package/docs/architecture.md +25 -27
  39. package/docs/binding-review-placement-design.md +29 -21
  40. package/docs/capability-planning-components.md +93 -0
  41. package/docs/capability-projection.md +7 -0
  42. package/docs/client-ui.md +88 -29
  43. package/docs/client-visual-contract.md +65 -0
  44. package/docs/context-management.md +22 -0
  45. package/docs/context-optimization-plan.md +353 -0
  46. package/docs/context-public-interface-coverage.md +25 -0
  47. package/docs/durability-design.md +31 -14
  48. package/docs/evaluation.md +41 -6
  49. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  50. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  51. package/docs/installation.md +50 -8
  52. package/docs/program-data-plane.md +5 -3
  53. package/docs/publishing.md +5 -5
  54. package/docs/release-coverage.md +73 -0
  55. package/docs/release-notes.md +1 -1
  56. package/docs/review-procedure.md +56 -0
  57. package/docs/runtime-reference.md +48 -16
  58. package/docs/semantic-validation.md +14 -2
  59. package/docs/stateful-binding-updates-proposal.md +2 -2
  60. package/docs/user-bindings.en.md +2 -2
  61. package/docs/user-bindings.md +3 -3
  62. package/docs/value-wire.md +11 -1
  63. package/docs/verification-optimization-work.md +52 -15
  64. package/docs/verification.md +30 -86
  65. package/index.js +147 -23
  66. package/internal/binding-toggle-receipt.js +24 -0
  67. package/internal/capability-command-receipt.js +29 -0
  68. package/internal/capability-environment.js +1247 -0
  69. package/internal/capability-plan.js +285 -0
  70. package/internal/capability-policy.js +96 -0
  71. package/internal/capability-statistics-store.js +180 -0
  72. package/internal/capability-statistics.js +210 -0
  73. package/internal/compiler-platform.js +5 -3
  74. package/internal/compiler-service.js +2 -9
  75. package/internal/config-spec.js +79 -9
  76. package/internal/cordis-tools-owner.js +22 -19
  77. package/internal/direct-surface-owner.js +29 -29
  78. package/internal/dynamic-environment-runtime.js +9 -9
  79. package/internal/edit-transport-owner.js +183 -139
  80. package/internal/execution-provider-owner.js +160 -0
  81. package/internal/execution-seam-compat.js +76 -120
  82. package/internal/isolated-worker.js +12 -8
  83. package/internal/kernel-worker.js +104 -32
  84. package/internal/native-root-dynamic.js +12 -13
  85. package/internal/program-bindings.js +190 -0
  86. package/internal/recovery-tips.js +53 -23
  87. package/internal/rejected-cell-editor.js +8 -8
  88. package/internal/repl-observation-interest.js +11 -3
  89. package/internal/rpc-contract.js +1 -0
  90. package/internal/runtime-bridge-owner.js +351 -82
  91. package/internal/runtime-config.js +58 -0
  92. package/internal/runtime-contexts.js +85 -16
  93. package/internal/runtime-intrinsics.js +1 -0
  94. package/internal/runtime-messages.js +41 -0
  95. package/internal/sdk-projection.js +10 -82
  96. package/internal/session-cell-executor.js +42 -19
  97. package/internal/session-events.js +74 -4
  98. package/internal/session-journal-recovery.js +904 -184
  99. package/internal/session-journal-schema.js +6 -3
  100. package/internal/session-journal.js +31 -2
  101. package/internal/session-log-view.js +189 -43
  102. package/internal/session-runtime.js +175 -23
  103. package/internal/settings-compat.js +23 -6
  104. package/internal/stateful-module-compiler.js +3 -2
  105. package/internal/stateful-module-runtime.js +7 -3
  106. package/internal/stateful-root-runtime.js +9 -4
  107. package/internal/user-binding-authoring.js +2 -7
  108. package/internal/user-bindings-owner.js +47 -7
  109. package/internal/validated-parse-repair.js +1 -1
  110. package/internal/value-wire.js +68 -21
  111. package/internal/worker-cwd-virtualization.js +20 -18
  112. package/internal/worker-realm-surfaces.js +3 -3
  113. package/locale/en.json +6 -0
  114. package/locale/zh.json +6 -0
  115. package/package.json +55 -38
  116. package/src/client-authoring-view.js +286 -168
  117. package/src/client-binding-review.js +4 -1
  118. package/src/client-binding-toggle.js +18 -0
  119. package/src/client-capabilities-view.js +1341 -0
  120. package/src/client-capability-sort.js +29 -0
  121. package/src/client-code-editor.js +6 -0
  122. package/src/client-copy.js +881 -8
  123. package/src/client-feature-gates.js +15 -5
  124. package/src/client-host-compat.js +23 -2
  125. package/src/client-indicator-view.js +4 -6
  126. package/src/client-primitives.js +280 -0
  127. package/src/client-repl-view.js +23 -16
  128. package/src/client-settings-view.js +412 -69
  129. package/src/client-styles.js +193 -41
  130. package/src/client-tool-hover.js +185 -0
  131. package/src/client-tool-view.js +45 -11
  132. package/src/client-transport.js +44 -9
  133. package/src/client-workbench.js +9 -10
  134. package/src/client.js +148 -68
  135. package/assets/ptc-plus-bindings-en.png +0 -0
  136. package/assets/ptc-plus-bindings-zh.png +0 -0
  137. package/assets/ptc-plus-repair-en.png +0 -0
  138. package/assets/ptc-plus-repair-zh.png +0 -0
  139. package/client.js +0 -31857
@@ -1,5 +1,19 @@
1
1
  /** Deterministic capability metadata derived from the current DSH tool view. */
2
2
  import { record, text } from './record-utils.js'
3
+ import { createHash } from 'node:crypto'
4
+ import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
5
+
6
+ /** Derive SDK contracts from the public, scope-qualified tool registry. */
7
+ export function toolSdkSchemas(tools, scope) {
8
+ return tools.schemas(scope).filter(schema => schema.name !== 'run_code').map(schema => {
9
+ const definition = tools.get(schema.name, scope)
10
+ const output = snapshotJsonValue(definition?.output?.schema)
11
+ if (output === undefined) {
12
+ throw new TypeError(`ptc-plus: tool "${schema.name}" output schema must be lossless JSON before SDK projection`)
13
+ }
14
+ return { ...schema, output }
15
+ })
16
+ }
3
17
 
4
18
  const COMPLETENESS = new Set(['complete', 'bounded', 'incremental', 'open-world', 'unknown'])
5
19
  const SOURCE_KINDS = new Set(['authored', 'runtime', 'tests', 'docs'])
@@ -185,3 +199,179 @@ export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
185
199
  budget,
186
200
  }
187
201
  }
202
+
203
+ const SEARCH_DEFAULT_BYTES = 16_384
204
+ const SEARCH_MIN_BYTES = 512
205
+ const SEARCH_STOP_WORDS = new Set('a an and are as at be by can could do for from how i in is it me of on or please that the this to use using with would you 我 帮 帮我 请 的 了 和 与 在 如何 怎么 可以'.split(' '))
206
+ const SEARCH_SEGMENTER = new Intl.Segmenter('zh', { granularity: 'word' })
207
+
208
+ function taskTerms(value) {
209
+ const separated = value.normalize('NFKC')
210
+ .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1 $2')
211
+ .replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')
212
+ .toLowerCase()
213
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
214
+ return [...new Set([...SEARCH_SEGMENTER.segment(separated)]
215
+ .filter(part => part.isWordLike && !SEARCH_STOP_WORDS.has(part.segment))
216
+ .map(part => part.segment))]
217
+ }
218
+
219
+ function searchKeywords(value) {
220
+ if (value === undefined) return ''
221
+ if (typeof value === 'string') return value
222
+ if (Array.isArray(value) && value.every(item => typeof item === 'string')) return value.join(' ')
223
+ throw new TypeError('ptc-plus: capability search keywords must be a string or string array')
224
+ }
225
+
226
+ // Search budgets measure JSON, so non-JSON metadata cannot be silently converted or dropped.
227
+ function searchJson(value) {
228
+ const ancestors = new Set()
229
+ const visit = (item) => {
230
+ if (item === null || typeof item === 'string' || typeof item === 'boolean') return
231
+ if (typeof item === 'number' && Number.isFinite(item)) return
232
+ if (typeof item !== 'object') throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
233
+ if (ancestors.has(item)) throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
234
+ const prototype = Object.getPrototypeOf(item)
235
+ if (prototype !== (Array.isArray(item) ? Array.prototype : Object.prototype) && prototype !== null) {
236
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
237
+ }
238
+ ancestors.add(item)
239
+ for (const key of Reflect.ownKeys(item)) {
240
+ if (Array.isArray(item) && key === 'length') continue
241
+ const descriptor = Object.getOwnPropertyDescriptor(item, key)
242
+ if (typeof key !== 'string' || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
243
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
244
+ }
245
+ visit(descriptor.value)
246
+ }
247
+ if (Array.isArray(item) && (Object.keys(item).length !== item.length
248
+ || Object.keys(item).some(key => !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length))) {
249
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
250
+ }
251
+ ancestors.delete(item)
252
+ }
253
+ visit(value)
254
+ return JSON.stringify(value, (_key, item) => item !== null && typeof item === 'object' && !Array.isArray(item)
255
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item)
256
+ }
257
+
258
+ function searchDigest(value) {
259
+ return createHash('sha256').update(value).digest('base64url')
260
+ }
261
+
262
+ function measuredSearchPage(value) {
263
+ value.usedBytes = 0
264
+ let bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
265
+ while (value.usedBytes !== bytes) {
266
+ value.usedBytes = bytes
267
+ bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
268
+ }
269
+ return value
270
+ }
271
+
272
+ /** Derive one request-local lexical index from owner metadata; never owns invocation. */
273
+ export function createCapabilitySearcher(metadata) {
274
+ const seen = new Set()
275
+ const snapshot = JSON.parse(searchJson(metadata))
276
+ const catalog = metadataEntries(snapshot).flatMap((entry) => {
277
+ const { members, ...owner } = entry
278
+ return members.map((member) => {
279
+ const symbol = `${entry.namespace}.${member.name}`
280
+ if (seen.has(symbol)) throw new TypeError(`ptc-plus: capability search has ambiguous symbol ${symbol}`)
281
+ seen.add(symbol)
282
+ const value = metadataSnapshot(JSON.parse(searchJson({ symbol, owner, contract: member })), 'capability search metadata')
283
+ const names = new Set(taskTerms(symbol))
284
+ const terms = new Set(taskTerms([
285
+ symbol, member.description ?? '', owner.description ?? '',
286
+ searchKeywords(member.keywords), searchKeywords(owner.keywords),
287
+ ].join(' ')))
288
+ return { value, names, terms }
289
+ })
290
+ }).sort((left, right) => left.value.symbol < right.value.symbol ? -1 : left.value.symbol > right.value.symbol ? 1 : 0)
291
+ const catalogId = searchDigest(JSON.stringify(catalog.map(item => item.value)))
292
+
293
+ return (args) => {
294
+ const params = record(args, 'capability search arguments')
295
+ const allowed = new Set(['query', 'budgetBytes', 'limit', 'cursor'])
296
+ if (Reflect.ownKeys(params).some(key => typeof key !== 'string' || !allowed.has(key)
297
+ || !Object.prototype.propertyIsEnumerable.call(params, key))) {
298
+ throw new TypeError('ptc-plus: capability search has an unknown argument')
299
+ }
300
+ const query = text(params.query, 'capability search query').normalize('NFKC').trim().toLowerCase()
301
+ const queryTerms = taskTerms(params.query)
302
+ const budgetBytes = params.budgetBytes === undefined ? SEARCH_DEFAULT_BYTES : params.budgetBytes
303
+ const limit = params.limit === undefined ? 10 : params.limit
304
+ if (!Number.isSafeInteger(budgetBytes) || budgetBytes < SEARCH_MIN_BYTES) {
305
+ throw new TypeError(`ptc-plus: capability search budgetBytes must be an integer of at least ${SEARCH_MIN_BYTES}`)
306
+ }
307
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('ptc-plus: capability search limit must be a positive integer')
308
+ const queryId = searchDigest(JSON.stringify({ query, terms: queryTerms }))
309
+ const ranked = catalog.flatMap((candidate) => {
310
+ const { symbol, owner, contract } = candidate.value
311
+ const coverage = queryTerms.filter(term => candidate.terms.has(term)).length
312
+ const nameCoverage = queryTerms.filter(term => candidate.names.has(term)).length
313
+ const rank = symbol.normalize('NFKC').toLowerCase() === query ? 0
314
+ : owner.namespace.normalize('NFKC').toLowerCase() === query ? 1
315
+ : contract.name.normalize('NFKC').toLowerCase() === query ? 2
316
+ : coverage === 0 ? undefined
317
+ : nameCoverage === queryTerms.length ? 3
318
+ : coverage === queryTerms.length ? 4 : 5
319
+ return rank === undefined ? [] : [{ ...candidate, rank, coverage, nameCoverage }]
320
+ }).sort((left, right) => left.rank - right.rank || right.coverage - left.coverage
321
+ || right.nameCoverage - left.nameCoverage
322
+ || (left.value.symbol < right.value.symbol ? -1 : left.value.symbol > right.value.symbol ? 1 : 0))
323
+
324
+ let offset = 0
325
+ if (params.cursor !== undefined) {
326
+ if (typeof params.cursor !== 'string' || !/^[A-Za-z0-9_-]+$/.test(params.cursor)) {
327
+ throw new TypeError('ptc-plus: capability search cursor is invalid')
328
+ }
329
+ let cursor
330
+ try { cursor = JSON.parse(Buffer.from(params.cursor, 'base64url').toString('utf8')) } catch {
331
+ throw new TypeError('ptc-plus: capability search cursor is invalid')
332
+ }
333
+ if (cursor?.version !== 1 || cursor.catalog !== catalogId || cursor.query !== queryId
334
+ || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0 || cursor.offset >= ranked.length
335
+ || Buffer.from(JSON.stringify(cursor)).toString('base64url') !== params.cursor) {
336
+ throw new TypeError('ptc-plus: capability search cursor is invalid or catalog/query changed')
337
+ }
338
+ offset = cursor.offset
339
+ }
340
+
341
+ const page = (matches, oversized, next) => measuredSearchPage({
342
+ matches, oversized, total: ranked.length, remaining: ranked.length - next,
343
+ cursor: next < ranked.length ? Buffer.from(JSON.stringify({
344
+ version: 1, catalog: catalogId, query: queryId, offset: next,
345
+ })).toString('base64url') : null,
346
+ budgetBytes, unit: 'utf8-json-bytes', usedBytes: 0,
347
+ })
348
+ let result = page([], [], offset)
349
+ for (let next = offset; next < ranked.length && next - offset < limit; next += 1) {
350
+ const candidate = ranked[next].value
351
+ const expanded = page([...result.matches, candidate], result.oversized, next + 1)
352
+ if (expanded.usedBytes <= budgetBytes) {
353
+ result = expanded
354
+ continue
355
+ }
356
+ const alone = page([candidate], [], next + 1)
357
+ if (alone.usedBytes <= budgetBytes) break
358
+ const omitted = {
359
+ symbol: candidate.symbol, requiredBytes: alone.usedBytes,
360
+ inspect: { symbols: [candidate.symbol], budget: 1 },
361
+ }
362
+ const bounded = page(result.matches, [...result.oversized, omitted], next + 1)
363
+ if (bounded.usedBytes > budgetBytes) {
364
+ if (next === offset) throw new RangeError(`ptc-plus: capability search needs at least ${bounded.usedBytes} budgetBytes for its continuation and inspect reference`)
365
+ break
366
+ }
367
+ result = bounded
368
+ }
369
+ if (result.usedBytes > budgetBytes) throw new RangeError(`ptc-plus: capability search needs at least ${result.usedBytes} budgetBytes for its continuation`)
370
+ return result
371
+ }
372
+ }
373
+
374
+ /** Return whole owner contracts within a UTF-8 JSON byte budget, including the envelope. */
375
+ export function capabilitySearch(metadata, args) {
376
+ return createCapabilitySearcher(metadata)(args)
377
+ }
@@ -1,34 +1,66 @@
1
- import { recoveryTipIdentity } from './runtime-messages.js'
1
+ import { PTC_STATE_NAMES, recoveryTipIdentity } from './runtime-messages.js'
2
2
 
3
3
  const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
4
4
  const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
5
+ const PLATFORM_FACTS = '`return { platform: process.platform, arch: process.arch, node: process.version, cwd: process.cwd(), execPath: process.execPath }`'
5
6
  const TIP_PREFIXES = Object.freeze({
6
7
  'repeated-binding-failure': 'The same binding failure has recurred.',
7
8
  'platform-command-failure': 'An executable, shell, or path failed in the current execution world.',
8
9
  })
9
10
 
10
11
  // TODO(dsh-tips-api): replace this local provider with an adapter after dsh-tips publishes a stable facts and decision interface.
11
- function tipHistory(view) {
12
- const active = new Map()
13
- const seen = new Set()
14
- const history = []
12
+ export function createRuntimeHistory() {
13
+ return { seenNames: {}, tips: {}, hadState: false, hadCatalog: false }
14
+ }
15
+
16
+ export function advanceRuntimeHistory(previous, { sections = [], notice, snapshot = false, catalog = false,
17
+ index, contextStep, lastSuccessfulRunIndex, resetSuccess = false }) {
18
+ let next = previous
19
+ const revise = () => {
20
+ if (next === previous) next = { ...previous, tips: { ...previous.tips }, seenNames: { ...previous.seenNames } }
21
+ }
22
+ if (resetSuccess) {
23
+ revise()
24
+ for (const [id, tip] of Object.entries(next.tips)) next.tips[id] = { ...tip, unresolved: 0 }
25
+ for (const [name, deliveredIndex] of Object.entries(next.seenNames)) {
26
+ const identity = recoveryTipIdentity(name)
27
+ if (deliveredIndex > (lastSuccessfulRunIndex ?? -1)) next.tips[identity.id].unresolved += 1
28
+ }
29
+ }
30
+ const hadState = snapshot || sections.some(section => PTC_STATE_NAMES.includes(section.name))
31
+ if ((hadState && !next.hadState) || (catalog && !next.hadCatalog)) {
32
+ revise()
33
+ next.hadState ||= hadState
34
+ next.hadCatalog ||= catalog
35
+ }
36
+ for (const name of [...sections.map(section => section.name), ...(notice === undefined ? [] : [notice])]) {
37
+ const identity = recoveryTipIdentity(name)
38
+ if (identity === undefined || Object.hasOwn(next.seenNames, name)) continue
39
+ revise()
40
+ next.seenNames[name] = index
41
+ const earlier = next.tips[identity.id]
42
+ next.tips[identity.id] = {
43
+ highestOrdinal: Math.max(earlier?.highestOrdinal ?? 0, identity.ordinal),
44
+ contextStep,
45
+ unresolved: (earlier?.unresolved ?? 0) + (index > (lastSuccessfulRunIndex ?? -1) ? 1 : 0),
46
+ }
47
+ }
48
+ return next
49
+ }
50
+
51
+ export function runtimeHistoryForView(view) {
52
+ if (view.runtimeHistory !== undefined) return view.runtimeHistory
53
+ let history = createRuntimeHistory()
54
+ history.hadState = view.ptcMessages?.some(record => record.form === 'snapshot') === true
55
+ history.hadCatalog = view.ptcMessages?.some(record => record.form === 'catalog') === true
15
56
  const snapshots = [
16
- ...view.systemPromptSnapshots,
57
+ ...(view.systemPromptSnapshots ?? []),
17
58
  ...(view.ptcMessages ?? []).filter(record => record.form === 'notice').map(record => ({
18
59
  ...record, sections: [{ name: record.name, text: record.text }],
19
60
  })),
20
61
  ].sort((left, right) => left.index - right.index)
21
62
  for (const snapshot of snapshots) {
22
- const next = new Map(snapshot.sections.map(section => [section.name, section.text]))
23
- for (const [name] of next) {
24
- if (active.has(name) || seen.has(name)) continue
25
- const identity = recoveryTipIdentity(name)
26
- if (identity === undefined) continue
27
- seen.add(name)
28
- history.push({ ...identity, index: snapshot.index, contextStep: snapshot.contextStep })
29
- }
30
- active.clear()
31
- for (const entry of next) active.set(...entry)
63
+ history = advanceRuntimeHistory(history, { ...snapshot, lastSuccessfulRunIndex: view.lastSuccessfulRunIndex })
32
64
  }
33
65
  return history
34
66
  }
@@ -62,8 +94,8 @@ function tipCandidate(view) {
62
94
  function renderTip(id, detailed, candidate = {}) {
63
95
  if (id === 'platform-command-failure') {
64
96
  return detailed
65
- ? `${TIP_PREFIXES[id]} Re-check the active execution world and the actual executable before retrying. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules.`
66
- : `${TIP_PREFIXES[id]} Inspect the executable or path in the current execution world and choose direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell.`
97
+ ? `${TIP_PREFIXES[id]} Re-check the active execution world and the actual executable before retrying. First inspect bounded runtime facts with ${PLATFORM_FACTS}; use the result to choose the executable and path. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules. Do not dump process.env.`
98
+ : `${TIP_PREFIXES[id]} Inspect bounded runtime facts with ${PLATFORM_FACTS}, then choose the executable and path in the current execution world. Use direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell. Do not dump process.env.`
67
99
  }
68
100
  if (!candidate.capability) {
69
101
  return `${TIP_PREFIXES[id]} Inspect the visible cell source and results for the local name, scope, initialization, or declaration conflict. Correct that expression before continuing; capability discovery does not list session-local variables.${detailed ? ' Earlier statements may have run; use current expression evidence without assuming a failed declaration initialized its binding.' : ''}`
@@ -79,14 +111,12 @@ export function latestRecoveryTip(view, config) {
79
111
  if (candidate === undefined) return undefined
80
112
  // Cooldown, escalation, and ordinal are per trigger kind: one kind's tip must
81
113
  // not suppress a different kind.
82
- const matching = tipHistory(view).filter(item => item.id === candidate.id)
83
- const lastTip = matching.at(-1)
114
+ const lastTip = runtimeHistoryForView(view).tips[candidate.id]
84
115
  if (lastTip !== undefined && view.contextStep - lastTip.contextStep < config.cooldownMessages) return undefined
85
- const unresolved = matching.filter(item => item.index > (view.lastSuccessfulRunIndex ?? -1))
86
- const ordinal = matching.reduce((highest, item) => Math.max(highest, item.ordinal), 0) + 1
116
+ const ordinal = (lastTip?.highestOrdinal ?? 0) + 1
87
117
  if (!Number.isSafeInteger(ordinal)) return undefined
88
118
  return {
89
119
  name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
90
- text: renderTip(candidate.id, unresolved.length >= config.escalationFailures, candidate),
120
+ text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate),
91
121
  }
92
122
  }
@@ -255,39 +255,39 @@ function assemble(source, replacements) {
255
255
  export function editRunCodeSchema() {
256
256
  return {
257
257
  name: 'edit_run_code',
258
- description: 'Edit the most recent eligible cell captured at dispatch and run the complete corrected cell. A successful edit becomes the next target. This reruns the entire cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
258
+ description: 'Call this from within a run_code cell. It accepts an edit of the most recent eligible cell captured at nested dispatch and returns { accepted, scheduled, target_call_seq } when scheduled. The corrected cell runs after the current run_code cell releases its worker; its result is reported with that cell, and a successful edit becomes the next target. This reruns the entire target cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
259
259
  parameters: {
260
260
  type: 'object',
261
261
  additionalProperties: false,
262
262
  properties: {
263
263
  edits: {
264
- type: 'array', minItems: 1, maxItems: EDIT_LIMITS.exactEdits,
264
+ type: 'array',
265
265
  description: 'Atomic exact replacements, all resolved against the original target cell.',
266
266
  items: {
267
267
  type: 'object', additionalProperties: false,
268
268
  properties: {
269
- old_string: { type: 'string', minLength: 1, description: 'Exact text in the target cell. It must occur exactly once.' },
269
+ old_string: { type: 'string', description: 'Nonempty exact text in the target cell. It must occur exactly once.' },
270
270
  new_string: { type: 'string', description: 'Literal replacement text. Use an empty string to delete the match.' },
271
271
  },
272
272
  required: ['old_string', 'new_string'],
273
273
  },
274
274
  },
275
275
  regex_edits: {
276
- type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
276
+ type: 'array',
277
277
  description: 'Atomic regular-expression replacements, all matched against the original target cell with JavaScript pattern and replacement semantics; each replacement is a JavaScript template (see replacement).',
278
278
  items: {
279
279
  type: 'object', additionalProperties: false,
280
280
  properties: {
281
- pattern: { type: 'string', minLength: 1, description: 'JavaScript regular-expression source without delimiters or flags (for example \\d+px, not /\\d+px/). Zero-length matches are rejected.' },
282
- flags: { type: 'string', pattern: '^(?!.*(.).*\\1)[gimsu]*$', description: 'Unique JavaScript g, i, m, s, or u flags. Global matching is always applied.' },
281
+ pattern: { type: 'string', description: 'Nonempty JavaScript regular-expression source without delimiters or flags (for example \\d+px, not /\\d+px/). Zero-length matches are rejected.' },
282
+ flags: { type: 'string', description: 'Unique JavaScript g, i, m, s, or u flags. Global matching is always applied.' },
283
283
  replacement: { type: 'string', description: 'JavaScript replacement template. Reference captures as $1, $2, ... or $<name> (for example "<$1>" inserts capture 1); backslash escapes such as \\1 are NOT capture references and stay literal text; write a literal dollar sign as $$.' },
284
- expected_matches: { type: 'integer', minimum: 1, maximum: EDIT_LIMITS.regexMatches, description: 'Exact match count required before any replacement is applied.' },
284
+ expected_matches: { type: 'integer', description: 'Positive exact match count required before any replacement is applied.' },
285
285
  },
286
286
  required: ['pattern', 'flags', 'replacement', 'expected_matches'],
287
287
  },
288
288
  },
289
289
  [EXPECTED_TARGET_CALL_SEQ]: {
290
- type: 'integer', minimum: 0,
290
+ type: 'integer',
291
291
  description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
292
292
  },
293
293
  },
@@ -12,6 +12,11 @@ export function createReplObservationInterest(ctx, initiallyEnabled, observe = a
12
12
  for (const watcher of [...watchers]) watcher.release()
13
13
  for (const controller of inspections) controller.abort()
14
14
  }
15
+ const releaseEvents = typeof ctx.on === 'function' ? ctx.effect(() => ctx.on('session/event', session => {
16
+ for (const watcher of [...watchers]) {
17
+ if (watcher.sessionId === session.id) watcher.changed()
18
+ }
19
+ }), 'ptc-plus: model source observation changes') : undefined
15
20
  const handler = async (endpoint, payload, signal) => {
16
21
  if (!enabled || !['watch', 'observe'].includes(endpoint) || typeof payload?.sessionId !== 'string'
17
22
  || payload.sessionId.length === 0 || payload.sessionId.length > 256
@@ -32,11 +37,13 @@ export function createReplObservationInterest(ctx, initiallyEnabled, observe = a
32
37
  }
33
38
  }
34
39
  return new Promise(resolve => {
35
- const watcher = { sessionId: payload.sessionId, release() {
40
+ const finish = value => {
36
41
  signal.removeEventListener('abort', watcher.release)
37
42
  watchers.delete(watcher)
38
- resolve({ ok: true, value: null })
39
- } }
43
+ resolve({ ok: true, value })
44
+ }
45
+ const watcher = { sessionId: payload.sessionId,
46
+ release: () => finish(null), changed: () => finish({ changed: true }) }
40
47
  watchers.add(watcher)
41
48
  signal.addEventListener('abort', watcher.release, { once: true })
42
49
  if (signal.aborted) watcher.release()
@@ -62,6 +69,7 @@ export function createReplObservationInterest(ctx, initiallyEnabled, observe = a
62
69
  async dispose() {
63
70
  enabled = false
64
71
  clear()
72
+ await releaseEvents?.()
65
73
  for (const release of [...registrations]) await release()
66
74
  if (typeof injection === 'function') await injection()
67
75
  else await injection?.dispose?.()
@@ -17,6 +17,7 @@ const resultSchema = Schema.union([
17
17
  export const RPC_CONTRACTS = Object.freeze({
18
18
  bindings: Object.freeze({ service: 'ptcPlusBindings' }),
19
19
  repl: Object.freeze({ service: 'ptcPlusRepl' }),
20
+ capabilities: Object.freeze({ service: 'ptcPlusCapabilities' }),
20
21
  })
21
22
 
22
23
  export function rpcDescriptor({ service }) {