picocode-core 0.9.200 → 0.9.202
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/package.json +1 -1
- package/src/controller.js +11 -4
- package/src/daemon-patch.js +2 -2
- package/src/daemon-projection.js +34 -0
- package/src/daemon-runtime.js +21 -5
- package/src/remote-controller.js +12 -6
package/package.json
CHANGED
package/src/controller.js
CHANGED
|
@@ -105,7 +105,7 @@ function errorText(err, limit) {
|
|
|
105
105
|
return String(err?.message || err).slice(0, limit)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
108
|
+
export function createController({ boot, run = runTurn, compactRun = compactHistory, peerDirectory } = {}) {
|
|
109
109
|
if (!Object.hasOwn(boot, 'participantAModel')) boot.participantAModel = boot.proposerModel ?? null
|
|
110
110
|
if (!Object.hasOwn(boot, 'participantBModel')) boot.participantBModel = boot.reviewerModel ?? null
|
|
111
111
|
const { on, emit } = createEmitter()
|
|
@@ -599,7 +599,7 @@ export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
|
599
599
|
return next
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
async function compact(instructions = '') {
|
|
602
|
+
async function compact(instructions = '', { automatic = false } = {}) {
|
|
603
603
|
if (state.busy || state.compacting || peerTransition) return flash('finish or interrupt the current turn first')
|
|
604
604
|
const current = state.derived
|
|
605
605
|
if (current.providerHistory.length < 4) return flash('nothing to compact yet')
|
|
@@ -618,7 +618,7 @@ export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
|
618
618
|
const keepFrom = compactionKeepFrom(current, state.model.context)
|
|
619
619
|
let streamed = ''
|
|
620
620
|
try {
|
|
621
|
-
const raw = await
|
|
621
|
+
const raw = await compactRun({
|
|
622
622
|
history: current.providerHistory,
|
|
623
623
|
modelName: state.model.name,
|
|
624
624
|
...(state.model.provider === 'codex' && { speed: speedApplies() ? state.speed : 'standard' }),
|
|
@@ -631,6 +631,7 @@ export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
|
631
631
|
set({ compactStatus: compactProgress(streamed) })
|
|
632
632
|
},
|
|
633
633
|
})
|
|
634
|
+
controller.signal.throwIfAborted()
|
|
634
635
|
const summary = formatCompactSummary(raw)
|
|
635
636
|
if (!summary) throw new Error('empty summary')
|
|
636
637
|
if (summarySections(summary) < 5) throw new Error('malformed summary, conversation left untouched')
|
|
@@ -638,6 +639,12 @@ export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
|
638
639
|
reDerive()
|
|
639
640
|
set({ compactOutcome: 'done' })
|
|
640
641
|
flash('compacted · recent messages kept verbatim')
|
|
642
|
+
if (automatic) {
|
|
643
|
+
noteSystem(
|
|
644
|
+
'[system notification] this conversation was automatically compacted to free context. Continue any unfinished work from where you left off. If the task is already complete or you are waiting for user input, stop. Do not recap or acknowledge the compaction.',
|
|
645
|
+
{ wake: true },
|
|
646
|
+
)
|
|
647
|
+
}
|
|
641
648
|
} catch (err) {
|
|
642
649
|
set({ compactOutcome: controller.signal.aborted ? 'cancelled' : 'failed' })
|
|
643
650
|
if (controller.signal.aborted) flash('compaction cancelled')
|
|
@@ -667,7 +674,7 @@ export function createController({ boot, run = runTurn, peerDirectory } = {}) {
|
|
|
667
674
|
const ratio = used / limit
|
|
668
675
|
if (ratio >= 0.85) {
|
|
669
676
|
flash(`context ${Math.round(ratio * 100)}% full · auto-compacting`)
|
|
670
|
-
compact()
|
|
677
|
+
compact('', { automatic: true })
|
|
671
678
|
}
|
|
672
679
|
}
|
|
673
680
|
|
package/src/daemon-patch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util'
|
|
2
2
|
|
|
3
3
|
export function diffSnapshot(previous, next, path = [], patches = []) {
|
|
4
|
-
if (
|
|
4
|
+
if (Object.is(previous, next)) return patches
|
|
5
5
|
if (typeof previous === 'string' && typeof next === 'string' && next.startsWith(previous)) {
|
|
6
6
|
patches.push({ op: 'appendText', path, value: next.slice(previous.length) })
|
|
7
7
|
} else if (Array.isArray(previous) && Array.isArray(next)) {
|
|
@@ -12,7 +12,7 @@ export function diffSnapshot(previous, next, path = [], patches = []) {
|
|
|
12
12
|
} else if (previous && next && Object.getPrototypeOf(previous) === Object.prototype && Object.getPrototypeOf(next) === Object.prototype) {
|
|
13
13
|
for (const key of Object.keys(previous)) if (!(key in next)) patches.push({ op: 'delete', path: [...path, key] })
|
|
14
14
|
for (const key of Object.keys(next)) diffSnapshot(previous[key], next[key], [...path, key], patches)
|
|
15
|
-
} else patches.push({ op: 'set', path, value: next })
|
|
15
|
+
} else if (!isDeepStrictEqual(previous, next)) patches.push({ op: 'set', path, value: next })
|
|
16
16
|
return patches
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { plain } from './daemon-wire.js'
|
|
2
|
+
|
|
3
|
+
export const defaultTranscriptLimit = 100
|
|
4
|
+
const stateExcluded = new Set(['events', 'persisted', 'derived', 'attachments', 'rewindUndo'])
|
|
5
|
+
const derivedExcluded = new Set(['transcript', 'providerHistory', 'historyEventIds', 'loadedContext', 'toolItems', 'peerMessages', 'consumedPeerIds', 'pendingPeerMessages', 'deferredConsumedPeers', 'openToolCalls', 'trimmedToolIds', 'toolTrimVersions'])
|
|
6
|
+
const agentExcluded = new Set(['events', 'timeline', 'options', 'controller', 'done', 'resolve'])
|
|
7
|
+
const select = (value, excluded) => Object.fromEntries(Object.entries(value || {}).filter(([key]) => !excluded.has(key)))
|
|
8
|
+
|
|
9
|
+
export function createProjection() {
|
|
10
|
+
const clones = new WeakMap()
|
|
11
|
+
const derivedCache = new WeakMap()
|
|
12
|
+
const windows = new WeakMap()
|
|
13
|
+
function immutable(value) {
|
|
14
|
+
if (!value || typeof value !== 'object') return plain(value)
|
|
15
|
+
if (!clones.has(value)) clones.set(value, plain(value))
|
|
16
|
+
return clones.get(value)
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
state(source, limit = defaultTranscriptLimit) {
|
|
20
|
+
const derived = source.derived || {}
|
|
21
|
+
if (!derivedCache.has(derived)) derivedCache.set(derived, plain(select(derived, derivedExcluded)))
|
|
22
|
+
const transcript = derived.transcript || []
|
|
23
|
+
let byLimit = windows.get(derived)
|
|
24
|
+
if (!byLimit) windows.set(derived, byLimit = new Map())
|
|
25
|
+
const offset = Math.max(0, transcript.length - limit)
|
|
26
|
+
if (!byLimit.has(limit)) byLimit.set(limit, transcript.slice(offset).map(immutable))
|
|
27
|
+
const attachments = new Map([...source.attachments || []].map(([key, value]) => [key, plain(Object.fromEntries(['kind', 'path', 'mediaType', 'hash', 'subject', 'root', 'url', 'selector', 'tag', 'component', 'fromLine', 'toLine', 'fromColumn', 'toColumn'].filter(k => value[k] !== undefined).map(k => [k, value[k]])))]))
|
|
28
|
+
return { ...plain(select(source, stateExcluded)), attachments, rewindUndo: !!source.rewindUndo, derived: { ...derivedCache.get(derived), transcript: byLimit.get(limit), transcriptOffset: offset, transcriptTotal: transcript.length } }
|
|
29
|
+
},
|
|
30
|
+
agents(values) {
|
|
31
|
+
return values.map(value => ({ ...plain(select(value, agentExcluded)), ...(value.timeline ? { timeline: value.timeline.map(immutable) } : {}), events: (value.events || []).filter(event => ['content', 'tool_executing', 'tool_complete', 'tool_error'].includes(event.type)).map(immutable) }))
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/daemon-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { plain } from './daemon-wire.js'
|
|
3
3
|
import { diffSnapshot } from './daemon-patch.js'
|
|
4
|
+
import { createProjection, defaultTranscriptLimit } from './daemon-projection.js'
|
|
4
5
|
import { forkSession, loadSession } from './session.js'
|
|
5
6
|
|
|
6
7
|
const events = ['change', 'derived', 'flash', 'input', 'question', 'turn', 'session', 'resumed', 'mcp', 'shells', 'git', 'project', 'image']
|
|
@@ -8,7 +9,11 @@ const queries = ['activity', 'shellRows', 'costSummary', 'speedApplies', 'effort
|
|
|
8
9
|
const busy = ctl => ctl.isWorking?.() || ctl.state.busy || ctl.state.compacting || ctl.agents?.list().some(a => ['running', 'queued'].includes(a.status))
|
|
9
10
|
const named = ctl => !!ctl.state.derived?.title?.trim()
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const projections = new WeakMap()
|
|
13
|
+
|
|
14
|
+
export function snapshot(ctl, limit = defaultTranscriptLimit) {
|
|
15
|
+
if (!projections.has(ctl)) projections.set(ctl, createProjection())
|
|
16
|
+
const projection = projections.get(ctl)
|
|
12
17
|
const boot = ctl.boot
|
|
13
18
|
const services = {}
|
|
14
19
|
for (const [service, methods] of Object.entries({ shells: ['list', 'running'], wakeups: ['list', 'pending'], git: ['status'], mcp: ['list'], commands: ['list'], skills: ['list'] })) {
|
|
@@ -22,7 +27,7 @@ export function snapshot(ctl) {
|
|
|
22
27
|
}
|
|
23
28
|
const cached = {}
|
|
24
29
|
for (const method of queries) if (typeof ctl[method] === 'function') cached[method] = plain(ctl[method]())
|
|
25
|
-
return { state:
|
|
30
|
+
return { state: projection.state(ctl.state, limit), boot: plain(Object.fromEntries(Object.entries(boot).filter(([key]) => !['refs', 'shells', 'wakeups', 'git', 'mcp', 'memory', 'commands', 'skills', 'tracker'].includes(key)))), services, cached, agents: projection.agents(ctl.agents?.list() || []), methods: Object.keys(ctl).filter(k => typeof ctl[k] === 'function' && !['on', 'shutdown'].includes(k)) }
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function createDaemonRuntime({ createSession, onEmpty = () => {}, batchMs = 16 }) {
|
|
@@ -38,13 +43,13 @@ export function createDaemonRuntime({ createSession, onEmpty = () => {}, batchMs
|
|
|
38
43
|
async function createRecord(client, options) {
|
|
39
44
|
const created = await createSession(options)
|
|
40
45
|
const ctl = created.controller || created
|
|
41
|
-
const record = { id: randomUUID(), ctl, dispose: created.dispose, views: new Set([client]), events: [], timer: null, unsub: [], baselines: new Map() }
|
|
46
|
+
const record = { id: randomUUID(), ctl, dispose: created.dispose, views: new Set([client]), events: [], timer: null, unsub: [], baselines: new Map(), limits: new Map() }
|
|
42
47
|
sessions.set(record.id, record)
|
|
43
48
|
for (const type of events) record.unsub.push(ctl.on(type, payload => publish(record, type, payload)))
|
|
44
49
|
return record
|
|
45
50
|
}
|
|
46
51
|
function fullSnapshot(record, client) {
|
|
47
|
-
const value = snapshot(record.ctl)
|
|
52
|
+
const value = snapshot(record.ctl, record.limits.get(client) ?? defaultTranscriptLimit)
|
|
48
53
|
const sequence = (record.baselines.get(client)?.sequence || 0) + 1
|
|
49
54
|
record.baselines.set(client, { snapshot: value, sequence })
|
|
50
55
|
return { snapshot: value, sequence }
|
|
@@ -52,11 +57,14 @@ export function createDaemonRuntime({ createSession, onEmpty = () => {}, batchMs
|
|
|
52
57
|
function flush(record) {
|
|
53
58
|
clearTimeout(record.timer)
|
|
54
59
|
record.timer = null
|
|
55
|
-
const
|
|
60
|
+
const snapshots = new Map()
|
|
56
61
|
const notifications = record.events.splice(0)
|
|
57
62
|
for (const client of record.views) {
|
|
58
63
|
const baseline = record.baselines.get(client)
|
|
59
64
|
if (!baseline) continue
|
|
65
|
+
const limit = record.limits.get(client) ?? defaultTranscriptLimit
|
|
66
|
+
if (!snapshots.has(limit)) snapshots.set(limit, snapshot(record.ctl, limit))
|
|
67
|
+
const next = snapshots.get(limit)
|
|
60
68
|
const sequence = baseline.sequence + 1
|
|
61
69
|
const patches = diffSnapshot(baseline.snapshot, next)
|
|
62
70
|
record.baselines.set(client, { snapshot: next, sequence })
|
|
@@ -81,6 +89,7 @@ export function createDaemonRuntime({ createSession, onEmpty = () => {}, batchMs
|
|
|
81
89
|
if (!record) return
|
|
82
90
|
record.views.delete(client)
|
|
83
91
|
record.baselines.delete(client)
|
|
92
|
+
record.limits.delete(client)
|
|
84
93
|
if (!record.views.size && !named(record.ctl)) await dispose(record)
|
|
85
94
|
}
|
|
86
95
|
return {
|
|
@@ -112,6 +121,13 @@ export function createDaemonRuntime({ createSession, onEmpty = () => {}, batchMs
|
|
|
112
121
|
if (!record) throw new Error('session is no longer running')
|
|
113
122
|
if (op === 'attach') { record.views.add(client); return { id, ...fullSnapshot(record, client) } }
|
|
114
123
|
if (op === 'snapshot') { if (!record.views.has(client)) throw new Error('attach first'); return fullSnapshot(record, client) }
|
|
124
|
+
if (op === 'history') {
|
|
125
|
+
if (!record.views.has(client)) throw new Error('attach first')
|
|
126
|
+
const [limit] = args
|
|
127
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('history limit must be a positive safe integer')
|
|
128
|
+
record.limits.set(client, Math.max(record.limits.get(client) ?? defaultTranscriptLimit, limit))
|
|
129
|
+
return fullSnapshot(record, client)
|
|
130
|
+
}
|
|
115
131
|
if (op === 'switch') {
|
|
116
132
|
const [method, value] = args
|
|
117
133
|
if (!record.views.has(client)) throw new Error('attach to the session first')
|
package/src/remote-controller.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { applyPatches } from './daemon-patch.js'
|
|
2
|
-
import { deriveState } from './derive.js'
|
|
3
2
|
|
|
4
3
|
export async function createRemoteController(connection, { id, options = {}, localBoot = {} } = {}) {
|
|
5
4
|
const initial = await connection.request(id ? 'attach' : 'create', id, id ? [] : [options])
|
|
@@ -62,13 +61,12 @@ export async function createRemoteController(connection, { id, options = {}, loc
|
|
|
62
61
|
if (nextId === id) { await clearPreview(); return }
|
|
63
62
|
const previous = id
|
|
64
63
|
const previousPreview = previewId
|
|
65
|
-
const next = await connection.request('attach', nextId)
|
|
64
|
+
const next = previousPreview === nextId ? null : await connection.request('attach', nextId)
|
|
66
65
|
previewId = null
|
|
67
66
|
if (previousPreview && previousPreview !== nextId) await connection.request('detach', previousPreview)
|
|
68
|
-
id =
|
|
67
|
+
id = nextId
|
|
69
68
|
controller.id = id
|
|
70
|
-
sequences.set(next.id, next.sequence)
|
|
71
|
-
apply(next.snapshot)
|
|
69
|
+
if (next) { sequences.set(next.id, next.sequence); apply(next.snapshot) }
|
|
72
70
|
await connection.request('detach', previous)
|
|
73
71
|
emit('project', boot)
|
|
74
72
|
emit('session', state.session)
|
|
@@ -91,7 +89,15 @@ export async function createRemoteController(connection, { id, options = {}, loc
|
|
|
91
89
|
emit('session', state.session)
|
|
92
90
|
emit('change', state)
|
|
93
91
|
}
|
|
94
|
-
controller.previewSteer = changes =>
|
|
92
|
+
controller.previewSteer = changes => call('controller', 'previewSteer', changes)
|
|
93
|
+
controller.loadHistory = async limit => {
|
|
94
|
+
const target = displayedId()
|
|
95
|
+
const reply = await connection.request('history', target, [Number.isFinite(limit) ? limit : Number.MAX_SAFE_INTEGER])
|
|
96
|
+
if (displayedId() !== target) return
|
|
97
|
+
sequences.set(target, reply.sequence)
|
|
98
|
+
apply(reply.snapshot)
|
|
99
|
+
emit('change', state)
|
|
100
|
+
}
|
|
95
101
|
function show(snapshot) {
|
|
96
102
|
apply(snapshot)
|
|
97
103
|
emit('project', boot)
|