dsh-ptc-plus 0.4.8 → 0.4.10

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 (62) hide show
  1. package/README.en.md +11 -3
  2. package/README.md +11 -3
  3. package/client.js +1151 -529
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +1 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +19 -0
  7. package/docs/adr/0019-plugin-settings-and-kill-switch.md +13 -2
  8. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  9. package/docs/adr/0023-global-user-bindings.md +1 -1
  10. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  11. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  12. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  13. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  14. package/docs/architecture.md +3 -3
  15. package/docs/binding-review-placement-design.md +29 -21
  16. package/docs/client-ui.md +20 -8
  17. package/docs/client-visual-contract.md +48 -0
  18. package/docs/durability-design.md +12 -0
  19. package/docs/installation.md +27 -6
  20. package/docs/runtime-reference.md +1 -1
  21. package/docs/user-bindings.en.md +2 -2
  22. package/docs/user-bindings.md +3 -3
  23. package/docs/verification-optimization-work.md +26 -0
  24. package/docs/verification.md +2 -0
  25. package/index.js +28 -7
  26. package/internal/compiler-platform.js +5 -3
  27. package/internal/compiler-service.js +2 -9
  28. package/internal/cordis-tools-owner.js +22 -19
  29. package/internal/direct-surface-owner.js +2 -0
  30. package/internal/edit-transport-owner.js +8 -5
  31. package/internal/execution-provider-owner.js +157 -0
  32. package/internal/execution-seam-compat.js +69 -113
  33. package/internal/isolated-worker.js +12 -8
  34. package/internal/recovery-tips.js +50 -21
  35. package/internal/runtime-bridge-owner.js +88 -57
  36. package/internal/runtime-contexts.js +13 -13
  37. package/internal/session-events.js +40 -4
  38. package/internal/session-journal-recovery.js +678 -181
  39. package/internal/session-log-view.js +189 -43
  40. package/internal/session-runtime.js +58 -10
  41. package/internal/settings-compat.js +12 -2
  42. package/internal/user-bindings-owner.js +6 -5
  43. package/locale/en.json +6 -0
  44. package/locale/zh.json +6 -0
  45. package/package.json +13 -14
  46. package/src/client-authoring-view.js +222 -141
  47. package/src/client-binding-review.js +2 -0
  48. package/src/client-code-editor.js +6 -0
  49. package/src/client-copy.js +10 -0
  50. package/src/client-feature-gates.js +12 -5
  51. package/src/client-host-compat.js +23 -2
  52. package/src/client-primitives.js +249 -0
  53. package/src/client-repl-view.js +3 -3
  54. package/src/client-settings-view.js +2 -29
  55. package/src/client-styles.js +92 -33
  56. package/src/client-tool-view.js +8 -3
  57. package/src/client-workbench.js +3 -3
  58. package/src/client.js +72 -46
  59. package/assets/ptc-plus-bindings-en.png +0 -0
  60. package/assets/ptc-plus-bindings-zh.png +0 -0
  61. package/assets/ptc-plus-repair-en.png +0 -0
  62. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -1,4 +1,4 @@
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/'
@@ -8,27 +8,58 @@ const TIP_PREFIXES = Object.freeze({
8
8
  })
9
9
 
10
10
  // 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 = []
11
+ export function createRuntimeHistory() {
12
+ return { seenNames: {}, tips: {}, hadState: false, hadCatalog: false }
13
+ }
14
+
15
+ export function advanceRuntimeHistory(previous, { sections = [], notice, snapshot = false, catalog = false,
16
+ index, contextStep, lastSuccessfulRunIndex, resetSuccess = false }) {
17
+ let next = previous
18
+ const revise = () => {
19
+ if (next === previous) next = { ...previous, tips: { ...previous.tips }, seenNames: { ...previous.seenNames } }
20
+ }
21
+ if (resetSuccess) {
22
+ revise()
23
+ for (const [id, tip] of Object.entries(next.tips)) next.tips[id] = { ...tip, unresolved: 0 }
24
+ for (const [name, deliveredIndex] of Object.entries(next.seenNames)) {
25
+ const identity = recoveryTipIdentity(name)
26
+ if (deliveredIndex > (lastSuccessfulRunIndex ?? -1)) next.tips[identity.id].unresolved += 1
27
+ }
28
+ }
29
+ const hadState = snapshot || sections.some(section => PTC_STATE_NAMES.includes(section.name))
30
+ if ((hadState && !next.hadState) || (catalog && !next.hadCatalog)) {
31
+ revise()
32
+ next.hadState ||= hadState
33
+ next.hadCatalog ||= catalog
34
+ }
35
+ for (const name of [...sections.map(section => section.name), ...(notice === undefined ? [] : [notice])]) {
36
+ const identity = recoveryTipIdentity(name)
37
+ if (identity === undefined || Object.hasOwn(next.seenNames, name)) continue
38
+ revise()
39
+ next.seenNames[name] = index
40
+ const earlier = next.tips[identity.id]
41
+ next.tips[identity.id] = {
42
+ highestOrdinal: Math.max(earlier?.highestOrdinal ?? 0, identity.ordinal),
43
+ contextStep,
44
+ unresolved: (earlier?.unresolved ?? 0) + (index > (lastSuccessfulRunIndex ?? -1) ? 1 : 0),
45
+ }
46
+ }
47
+ return next
48
+ }
49
+
50
+ export function runtimeHistoryForView(view) {
51
+ if (view.runtimeHistory !== undefined) return view.runtimeHistory
52
+ let history = createRuntimeHistory()
53
+ history.hadState = view.ptcMessages?.some(record => record.form === 'snapshot') === true
54
+ history.hadCatalog = view.ptcMessages?.some(record => record.form === 'catalog') === true
15
55
  const snapshots = [
16
- ...view.systemPromptSnapshots,
56
+ ...(view.systemPromptSnapshots ?? []),
17
57
  ...(view.ptcMessages ?? []).filter(record => record.form === 'notice').map(record => ({
18
58
  ...record, sections: [{ name: record.name, text: record.text }],
19
59
  })),
20
60
  ].sort((left, right) => left.index - right.index)
21
61
  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)
62
+ history = advanceRuntimeHistory(history, { ...snapshot, lastSuccessfulRunIndex: view.lastSuccessfulRunIndex })
32
63
  }
33
64
  return history
34
65
  }
@@ -79,14 +110,12 @@ export function latestRecoveryTip(view, config) {
79
110
  if (candidate === undefined) return undefined
80
111
  // Cooldown, escalation, and ordinal are per trigger kind: one kind's tip must
81
112
  // not suppress a different kind.
82
- const matching = tipHistory(view).filter(item => item.id === candidate.id)
83
- const lastTip = matching.at(-1)
113
+ const lastTip = runtimeHistoryForView(view).tips[candidate.id]
84
114
  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
115
+ const ordinal = (lastTip?.highestOrdinal ?? 0) + 1
87
116
  if (!Number.isSafeInteger(ordinal)) return undefined
88
117
  return {
89
118
  name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
90
- text: renderTip(candidate.id, unresolved.length >= config.escalationFailures, candidate),
119
+ text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate),
91
120
  }
92
121
  }
@@ -147,6 +147,7 @@ export function createRuntimeBridgeOwner({
147
147
  sessionConfig,
148
148
  userBindingsCwd,
149
149
  observeSession,
150
+ readSessionLog = async session => session,
150
151
  maxNestedRunCodeDepth,
151
152
  presentationGeneration,
152
153
  sessionId,
@@ -172,7 +173,7 @@ export function createRuntimeBridgeOwner({
172
173
  const withInitiator = ctx.agents === undefined || typeof ctx.agents.withInitiator !== 'function'
173
174
  ? undefined
174
175
  : (agent, operation) => ctx.agents.withInitiator(agent, operation)
175
- const sessions = new SessionRuntime(sessionConfig, { withInitiator, userBindingsCwd, observeSession })
176
+ const sessions = new SessionRuntime(sessionConfig, { withInitiator, userBindingsCwd, observeSession, readSessionLog })
176
177
  let currentConfig = sessions.config
177
178
  const patchedDefinitions = new Map()
178
179
  const pending = new WeakMap()
@@ -364,16 +365,18 @@ export function createRuntimeBridgeOwner({
364
365
  }
365
366
  if (patchedDefinitions.has(definition)) return
366
367
  const output = definition.output
367
- const originalPresentationMeta = output.presentationMeta
368
+ const ownPresentationMeta = Object.getOwnPropertyDescriptor(output, 'presentationMeta')
368
369
  const patchedPresentationMeta = (args, value) => {
369
- if (!active) {
370
- return originalPresentationMeta === undefined
371
- ? undefined
372
- : originalPresentationMeta(args, value)
373
- }
370
+ const prototype = Object.getPrototypeOf(output)
371
+ const originalPresentationMeta = ownPresentationMeta === undefined
372
+ ? prototype === null ? undefined : Reflect.get(prototype, 'presentationMeta', output)
373
+ : Object.hasOwn(ownPresentationMeta, 'value')
374
+ ? ownPresentationMeta.value
375
+ : ownPresentationMeta.get?.call(output)
374
376
  const base = originalPresentationMeta === undefined
375
377
  ? undefined
376
- : originalPresentationMeta(args, value)
378
+ : originalPresentationMeta.call(output, args, value)
379
+ if (!active) return base
377
380
  const current = scope.getStore()
378
381
  const settlement = current?.settlement
379
382
  if (settlement === undefined) return generatedRunCodeDescriptionMeta(args, base)
@@ -417,15 +420,15 @@ export function createRuntimeBridgeOwner({
417
420
  patchedDefinitions.set(definition, {
418
421
  definition,
419
422
  output,
420
- originalPresentationMeta,
423
+ ownPresentationMeta,
421
424
  patchedPresentationMeta,
422
425
  ownExecute,
423
426
  patchedExecute,
424
427
  })
425
428
  } catch (error) {
426
429
  if (presentationPatched && output.presentationMeta === patchedPresentationMeta) {
427
- if (originalPresentationMeta === undefined) delete output.presentationMeta
428
- else output.presentationMeta = originalPresentationMeta
430
+ if (ownPresentationMeta === undefined) delete output.presentationMeta
431
+ else Object.defineProperty(output, 'presentationMeta', ownPresentationMeta)
429
432
  }
430
433
  throw new Error(`ptc-plus: cannot attach the session journal to run_code results: ${error.message}`)
431
434
  }
@@ -439,10 +442,10 @@ export function createRuntimeBridgeOwner({
439
442
  return runIsolated(request, child.executionToken, child.depth, child.cellConfig, child.functions)
440
443
  }
441
444
  }
442
- if (!active) return seam.invokeUpstream(request)
445
+ if (!active) return seam.runUpstream(request)
443
446
  const current = scope.getStore()
444
- if (current === undefined) return seam.invokeUpstream(request)
445
- const projected = projectBindings(request, 0, current)
447
+ if (current === undefined) return seam.runUpstream(request)
448
+ const projected = projectBindings(request, 0, current, undefined, current.submission?.config ?? currentConfig)
446
449
  return sessions.runTentative(current, {
447
450
  ...projected.request,
448
451
  executionToken: current,
@@ -455,7 +458,26 @@ export function createRuntimeBridgeOwner({
455
458
  .finally(projected.release)
456
459
  }
457
460
 
458
- const releaseSeam = seam.takeOver(patchedRun)
461
+ const releaseSeam = seam.installExecute(patchedRun)
462
+
463
+ const withSubmission = async (exec, operation) => {
464
+ const id = sessionId(exec.agent)
465
+ const inherited = scope.getStore()
466
+ if (exec.parent !== undefined || id === undefined || inherited?.submission?.sessionId === id) return operation()
467
+ const submission = sessions.beginSubmission({ id }, exec.signal)
468
+ try {
469
+ return await scope.run({ ...inherited, submission }, operation)
470
+ } finally {
471
+ sessions.releaseSubmission(submission)
472
+ }
473
+ }
474
+
475
+ const readSubmissionLog = async (session, signal) => {
476
+ const inherited = scope.getStore()
477
+ const sessionLog = inherited?.sessionLog ?? await readSessionLog(session, signal)
478
+ if (inherited?.submission !== undefined) sessions.assertSubmission(inherited.submission)
479
+ return sessionLog
480
+ }
459
481
 
460
482
  return Object.freeze({
461
483
  config: sessions.config,
@@ -464,8 +486,10 @@ export function createRuntimeBridgeOwner({
464
486
  currentConfig = sessions.config
465
487
  },
466
488
  observeRepl(id, memory, signal) { return sessions.observe(id, memory, signal) },
489
+ withSubmission,
490
+ readSubmissionLog,
467
491
  // A composite tool's outer result owns the final durability decision.
468
- async executeTentative(callSeq, operation) {
492
+ async executeTentative(callSeq, operation, sessionLog) {
469
493
  const settlement = {
470
494
  current: undefined,
471
495
  innerConfirmed: false,
@@ -480,7 +504,9 @@ export function createRuntimeBridgeOwner({
480
504
  }
481
505
  try {
482
506
  const result = await scope.run({
507
+ ...(scope.getStore()?.submission === undefined ? {} : { submission: scope.getStore().submission }),
483
508
  ...(callSeq === undefined ? {} : { persistedCallSeq: callSeq }),
509
+ ...(sessionLog === undefined ? {} : { sessionLog }),
484
510
  deferredSettlement: settlement,
485
511
  }, operation)
486
512
  return Object.freeze({ result, finalize })
@@ -489,46 +515,51 @@ export function createRuntimeBridgeOwner({
489
515
  throw error
490
516
  }
491
517
  },
492
- handleExecute(exec, next, executionArguments = exec.arguments, userBindings = undefined) {
518
+ async handleExecute(exec, next, executionArguments = exec.arguments, userBindings = undefined) {
493
519
  if (exec.parent !== undefined) return scope.run(undefined, next)
494
- const id = sessionId(exec.agent)
495
- if (id === undefined) return next()
496
- patchRunCodeDefinition(exec.agent)
497
- const inherited = scope.getStore()
498
- const persistedCallSeq = inherited?.persistedCallSeq
499
- const current = {
500
- id,
501
- callId: String(exec.callId),
502
- ...(persistedCallSeq === undefined ? {} : { persistedCallSeq }),
503
- ...(inherited?.deferredSettlement === undefined
504
- ? {}
505
- : { deferredSettlement: inherited.deferredSettlement }),
506
- session: exec.agent?.session,
507
- agent: exec.agent,
508
- executionArguments,
509
- ...(userBindings === undefined ? {} : { userBindings }),
510
- }
511
- if (current.deferredSettlement !== undefined) {
512
- current.deferredSettlement.current = current
513
- }
514
- pending.set(exec, current)
515
- return scope.run(current, async () => {
516
- const result = await next()
517
- const settlement = current.settlement
518
- if (result?.isError === true && settlement !== undefined) {
519
- let meta = withJournal(result.meta, settlement.journal)
520
- if (settlement.recoveryBoundaries !== undefined) {
521
- meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
522
- }
523
- if (settlement.rewrites !== undefined) meta = withRewrites(meta, settlement.rewrites)
524
- if (settlement.userBindings !== undefined) {
525
- meta = withUserBindingsSnapshot(meta, settlement.userBindings)
526
- }
527
- meta = withReplMemorySnapshot(meta, settlement.replMemory, presentationGeneration)
528
- meta = withDraftCapability(meta, current)
529
- return { ...result, meta }
520
+ return withSubmission(exec, async () => {
521
+ const id = sessionId(exec.agent)
522
+ if (id === undefined) return next()
523
+ patchRunCodeDefinition(exec.agent)
524
+ const inherited = scope.getStore()
525
+ const persistedCallSeq = inherited?.persistedCallSeq
526
+ const current = {
527
+ id,
528
+ submission: inherited?.submission,
529
+ callId: String(exec.callId),
530
+ ...(persistedCallSeq === undefined ? {} : { persistedCallSeq }),
531
+ ...(inherited?.deferredSettlement === undefined
532
+ ? {}
533
+ : { deferredSettlement: inherited.deferredSettlement }),
534
+ session: exec.agent?.session,
535
+ sessionLog: await readSubmissionLog(exec.agent?.session, exec.signal),
536
+ agent: exec.agent,
537
+ executionArguments,
538
+ ...(userBindings === undefined ? {} : { userBindings }),
539
+ }
540
+ sessions.assertSubmission(current.submission)
541
+ if (current.deferredSettlement !== undefined) {
542
+ current.deferredSettlement.current = current
530
543
  }
531
- return result
544
+ pending.set(exec, current)
545
+ return scope.run(current, async () => {
546
+ const result = await next()
547
+ const settlement = current.settlement
548
+ if (result?.isError === true && settlement !== undefined) {
549
+ let meta = withJournal(result.meta, settlement.journal)
550
+ if (settlement.recoveryBoundaries !== undefined) {
551
+ meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
552
+ }
553
+ if (settlement.rewrites !== undefined) meta = withRewrites(meta, settlement.rewrites)
554
+ if (settlement.userBindings !== undefined) {
555
+ meta = withUserBindingsSnapshot(meta, settlement.userBindings)
556
+ }
557
+ meta = withReplMemorySnapshot(meta, settlement.replMemory, presentationGeneration)
558
+ meta = withDraftCapability(meta, current)
559
+ return { ...result, meta }
560
+ }
561
+ return result
562
+ })
532
563
  })
533
564
  },
534
565
  handleResult(exec, result) {
@@ -539,7 +570,7 @@ export function createRuntimeBridgeOwner({
539
570
  pending.delete(exec)
540
571
  const settlement = current?.settlement
541
572
  if (settlement === undefined) {
542
- sessions.noteNoop(id, exec.agent?.session, exec.callId)
573
+ sessions.noteNoop(id, current?.sessionLog, exec.callId)
543
574
  return
544
575
  }
545
576
  const meta = result?.meta
@@ -586,8 +617,8 @@ export function createRuntimeBridgeOwner({
586
617
  releaseSeam()
587
618
  for (const patched of patchedDefinitions.values()) {
588
619
  if (patched.output.presentationMeta === patched.patchedPresentationMeta) {
589
- if (patched.originalPresentationMeta === undefined) delete patched.output.presentationMeta
590
- else patched.output.presentationMeta = patched.originalPresentationMeta
620
+ if (patched.ownPresentationMeta === undefined) delete patched.output.presentationMeta
621
+ else Object.defineProperty(patched.output, 'presentationMeta', patched.ownPresentationMeta)
591
622
  }
592
623
  if (patched.patchedExecute !== undefined
593
624
  && patched.definition.execute === patched.patchedExecute) {
@@ -1,5 +1,5 @@
1
- import { latestRecoveryTip } from './recovery-tips.js'
2
- import { projectSessionLog, systemPromptSnapshotSections } from './session-log-view.js'
1
+ import { latestRecoveryTip, runtimeHistoryForView } from './recovery-tips.js'
2
+ import { projectSessionLog, runtimeMessageFacts, systemPromptSnapshotSections } from './session-log-view.js'
3
3
  import {
4
4
  PTC_BINDING_CATALOG, PTC_DELIVERY_CONTEXT, PTC_STATE_NAMES, readRuntimeMessage,
5
5
  runtimeBindingCatalogMessage, runtimeNoticeMessage, runtimeStateMessage,
@@ -21,7 +21,7 @@ function continuationFeedback(view) {
21
21
 
22
22
  /** Build all dynamic PTC contexts from one session-log projection. */
23
23
  export function sessionRuntimeContexts(agent, tipConfig, options = {}) {
24
- const view = projectSessionLog(agent)
24
+ const view = (options.sessionLogView ?? projectSessionLog)(agent)
25
25
  const rewrite = continuationFeedback(view)
26
26
  const tip = latestRecoveryTip(view, tipConfig)
27
27
  const cordisRecovery = options.cordisRecoveryRequired?.(view) === true
@@ -42,13 +42,14 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
42
42
  const sections = contexts.filter(context => PTC_STATE_NAMES.includes(context.name) && context.name !== PTC_BINDING_CATALOG)
43
43
  const catalog = contexts.find(context => context.name === PTC_BINDING_CATALOG)
44
44
  const records = view.visibleRuntimeMessages
45
- const owned = records.filter(record => record.producer === 'ptc-plus' && record.form === 'snapshot').at(-1)
46
- const legacy = records.filter(record => record.producer === 'aggregate').at(-1)
45
+ const facts = view.visibleRuntimeFacts ?? runtimeMessageFacts(records)
46
+ const history = runtimeHistoryForView(view)
47
+ const owned = facts.snapshot
48
+ const legacy = facts.aggregate
47
49
  const pendingAggregate = pending.map(systemPromptSnapshotSections).filter(sections => sections !== undefined).at(-1)
48
50
  const retained = owned?.sections
49
51
  ?? (pendingAggregate ?? legacy?.sections)?.filter(section => PTC_STATE_NAMES.includes(section.name))
50
- const hadState = view.ptcMessages.some(record => record.form === 'snapshot')
51
- || view.systemPromptSnapshots.some(record => record.sections.some(section => PTC_STATE_NAMES.includes(section.name)))
52
+ const hadState = history.hadState
52
53
  const messages = []
53
54
  const proposed = pending.map(readRuntimeMessage).filter(record => record !== undefined)
54
55
  const pendingState = proposed.filter(record => record.form === 'snapshot').at(-1)
@@ -58,23 +59,22 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
58
59
  messages.push(runtimeStateMessage(sections))
59
60
  }
60
61
  const previousCatalog = proposed.filter(record => record.form === 'catalog').at(-1)
61
- ?? records.filter(record => record.form === 'catalog').at(-1)
62
- const hadCatalog = view.ptcMessages.some(record => record.form === 'catalog')
62
+ ?? facts.catalog
63
+ const hadCatalog = history.hadCatalog
63
64
  if ((catalog !== undefined || previousCatalog !== undefined || hadCatalog)
64
65
  && JSON.stringify(previousCatalog?.sections) !== JSON.stringify(catalog === undefined ? [] : [catalog])) {
65
66
  messages.push(runtimeBindingCatalogMessage(catalog))
66
67
  }
67
68
  for (const tip of contexts.filter(context => context.name.startsWith('tools:ptc-plus-tip/'))) {
68
69
  if (proposed.some(record => record.form === 'notice' && record.name === tip.name)) continue
69
- if (view.ptcMessages.some(record => record.form === 'notice' && record.name === tip.name)
70
- || view.systemPromptSnapshots.some(snapshot => snapshot.sections.some(section => section.name === tip.name))) continue
70
+ if (Object.hasOwn(history.seenNames, tip.name)) continue
71
71
  messages.push(runtimeNoticeMessage(tip))
72
72
  }
73
73
  return messages
74
74
  }
75
75
 
76
76
  /** Passive message delivery follows the actual public assembly and accepted step. */
77
- export function createRuntimeMessageOwner(contextsForRequest) {
77
+ export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = projectSessionLog) {
78
78
  const requests = new WeakMap()
79
79
  const disposedAgents = new WeakSet()
80
80
  let disposed = false
@@ -99,7 +99,7 @@ export function createRuntimeMessageOwner(contextsForRequest) {
99
99
  if (disposed || decision.kind !== 'enter' || payload.signal?.aborted === true
100
100
  || request?.allowed !== true || request.context.signal !== payload.signal) return decision
101
101
  const messages = projectRuntimeMessages(
102
- projectSessionLog(payload.agent), contextsForRequest(request.context), decision.messages,
102
+ sessionLogView(payload.agent), contextsForRequest(request.context), decision.messages,
103
103
  )
104
104
  return messages.length === 0 ? decision : { ...decision, messages: [...decision.messages, ...messages] }
105
105
  },
@@ -1,7 +1,43 @@
1
- /** Read the immutable session log across current and legacy DSH session APIs. */
1
+ /** Read an explicit observation or the historical array-backed session contract. */
2
2
  export function sessionEvents(session) {
3
- if (typeof session?.snapshotEvents === 'function') {
4
- return session.snapshotEvents()
3
+ if (session === undefined || session === null) return undefined
4
+ const events = session.events
5
+ if (Array.isArray(events)) return events
6
+ if (Object.hasOwn(session, 'events')) throw new Error('ptc-plus: legacy session.events is not an event array')
7
+ throw new Error('ptc-plus: session history requires a public sessionQuery observation')
8
+ }
9
+
10
+ export function advanceSessionSurface(nodes, event) {
11
+ if (event?.surfaceOp === 'append') return [...nodes, event.seq]
12
+ if (event?.surfaceOp?.op !== 'replace') return nodes
13
+ const first = nodes.indexOf(event.surfaceOp.startSeq ?? event.surfaceOp.start)
14
+ const last = nodes.indexOf(event.surfaceOp.endSeq ?? event.surfaceOp.end)
15
+ if (first === -1 || last < first) return nodes
16
+ const next = [...nodes]
17
+ next.splice(first, last - first + 1, event.seq)
18
+ return next
19
+ }
20
+
21
+ export async function readSessionLog(session, query, signal) {
22
+ if (session === undefined || session === null || Array.isArray(session.events)) return session
23
+ if (typeof query?.observeSession !== 'function') {
24
+ throw new Error('ptc-plus: sessionQuery.observeSession is unavailable for this session')
25
+ }
26
+ const observation = await query.observeSession(session.id, { signal, projectionMode: 'none' })
27
+ try {
28
+ if (!Array.isArray(observation.events)) throw new Error('ptc-plus: sessionQuery observation did not publish events')
29
+ let nodes = session.surface?.nodes
30
+ if (!Array.isArray(nodes) || session.seq - 1 !== observation.cursor) {
31
+ nodes = []
32
+ for (const event of observation.events) nodes = advanceSessionSurface(nodes, event)
33
+ }
34
+ return Object.freeze({
35
+ id: session.id, header: observation.header, events: observation.events,
36
+ surface: Object.freeze({
37
+ nodes: Object.freeze([...nodes]),
38
+ }),
39
+ })
40
+ } finally {
41
+ observation[Symbol.dispose]()
5
42
  }
6
- return session?.events
7
43
  }