@pi-in-go/pigpen-ahp 0.1.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.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package pisession
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ParseLine decodes one session-file line; blank and malformed lines report false.
|
|
10
|
+
func ParseLine(line string) (Entry, bool) {
|
|
11
|
+
line = strings.TrimSpace(line)
|
|
12
|
+
if line == "" {
|
|
13
|
+
return nil, false
|
|
14
|
+
}
|
|
15
|
+
var e Entry
|
|
16
|
+
if err := json.Unmarshal([]byte(line), &e); err != nil || e == nil {
|
|
17
|
+
return nil, false
|
|
18
|
+
}
|
|
19
|
+
return e, true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func writeLine(w *bufio.Writer, e Entry) error {
|
|
23
|
+
raw, err := json.Marshal(e)
|
|
24
|
+
if err != nil {
|
|
25
|
+
return err
|
|
26
|
+
}
|
|
27
|
+
if _, err := w.Write(raw); err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
return w.WriteByte('\n')
|
|
31
|
+
}
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
// Package pisession reads and writes Pi's session files (JSONL, version 3): a header line, then
|
|
2
|
+
// entries forming a tree by id/parentId. It ports the parts of Pi's SessionManager
|
|
3
|
+
// (dist/core/session-manager.js, 0.87.1) that pi-ahp uses; the format is Pi's, so files it writes
|
|
4
|
+
// open in Pi and files Pi writes load here.
|
|
5
|
+
//
|
|
6
|
+
// Entries are kept as decoded JSON: Pi's session format is open-ended (extensions add custom
|
|
7
|
+
// entry types) and this package must round-trip what it does not understand.
|
|
8
|
+
package pisession
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"bufio"
|
|
12
|
+
"crypto/rand"
|
|
13
|
+
"encoding/hex"
|
|
14
|
+
"errors"
|
|
15
|
+
"fmt"
|
|
16
|
+
"os"
|
|
17
|
+
"path/filepath"
|
|
18
|
+
"regexp"
|
|
19
|
+
"strings"
|
|
20
|
+
"sync"
|
|
21
|
+
"time"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// CurrentVersion is the session format version this package writes.
|
|
25
|
+
const CurrentVersion = 3
|
|
26
|
+
|
|
27
|
+
// Entry is one session entry (or the header) as decoded JSON.
|
|
28
|
+
type Entry map[string]any
|
|
29
|
+
|
|
30
|
+
// Type is the entry's `type`.
|
|
31
|
+
func (e Entry) Type() string { s, _ := e["type"].(string); return s }
|
|
32
|
+
|
|
33
|
+
// ID is the entry id.
|
|
34
|
+
func (e Entry) ID() string { s, _ := e["id"].(string); return s }
|
|
35
|
+
|
|
36
|
+
// ParentID is the parent entry id ("" for a root).
|
|
37
|
+
func (e Entry) ParentID() string { s, _ := e["parentId"].(string); return s }
|
|
38
|
+
|
|
39
|
+
// Timestamp is the entry's ISO timestamp.
|
|
40
|
+
func (e Entry) Timestamp() string { s, _ := e["timestamp"].(string); return s }
|
|
41
|
+
|
|
42
|
+
// Message is the message of a "message" entry.
|
|
43
|
+
func (e Entry) Message() map[string]any {
|
|
44
|
+
if e.Type() != "message" {
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
m, _ := e["message"].(map[string]any)
|
|
48
|
+
return m
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Role is the role of a "message" entry's message.
|
|
52
|
+
func (e Entry) Role() string {
|
|
53
|
+
s, _ := e.Message()["role"].(string)
|
|
54
|
+
return s
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var validID = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$`)
|
|
58
|
+
|
|
59
|
+
// ValidateID applies Pi's session id rules.
|
|
60
|
+
func ValidateID(id string) error {
|
|
61
|
+
if !validID.MatchString(id) {
|
|
62
|
+
return errors.New("Session id must be non-empty, contain only alphanumeric characters, '-', '_', and '.', and start and end with an alphanumeric character")
|
|
63
|
+
}
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Options for a new session.
|
|
68
|
+
type Options struct {
|
|
69
|
+
// ID of the new session; generated when empty.
|
|
70
|
+
ID string
|
|
71
|
+
ParentSession string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Manager is a session: its entry tree, a leaf pointer and (optionally) its file.
|
|
75
|
+
//
|
|
76
|
+
// Like Pi's, a persistent session withholds its file until the first assistant message exists,
|
|
77
|
+
// then writes every entry so far in one go and appends from there on.
|
|
78
|
+
type Manager struct {
|
|
79
|
+
mu sync.Mutex
|
|
80
|
+
cwd string
|
|
81
|
+
dir string
|
|
82
|
+
file string
|
|
83
|
+
persist bool
|
|
84
|
+
flushed bool
|
|
85
|
+
sessionID string
|
|
86
|
+
entries []Entry // header first
|
|
87
|
+
byID map[string]Entry
|
|
88
|
+
leafID string
|
|
89
|
+
now func() time.Time
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func newID() string {
|
|
93
|
+
var b [16]byte
|
|
94
|
+
_, _ = rand.Read(b[:])
|
|
95
|
+
return hex.EncodeToString(b[:])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func uuidLike() string {
|
|
99
|
+
b := make([]byte, 16)
|
|
100
|
+
_, _ = rand.Read(b)
|
|
101
|
+
b[6] = (b[6] & 0x0f) | 0x70
|
|
102
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
103
|
+
h := hex.EncodeToString(b)
|
|
104
|
+
return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// InMemory creates a session that never touches disk.
|
|
108
|
+
func InMemory(cwd string, opts Options) (*Manager, error) {
|
|
109
|
+
return create(cwd, "", false, opts)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create makes a persistent session whose file will live under dir.
|
|
113
|
+
func Create(cwd, dir string, opts Options) (*Manager, error) {
|
|
114
|
+
return create(cwd, dir, true, opts)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func create(cwd, dir string, persist bool, opts Options) (*Manager, error) {
|
|
118
|
+
if opts.ID != "" {
|
|
119
|
+
if err := ValidateID(opts.ID); err != nil {
|
|
120
|
+
return nil, err
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
abs, err := filepath.Abs(cwd)
|
|
124
|
+
if err != nil {
|
|
125
|
+
abs = cwd
|
|
126
|
+
}
|
|
127
|
+
m := &Manager{cwd: abs, dir: dir, persist: persist, now: time.Now, byID: map[string]Entry{}}
|
|
128
|
+
if persist && dir != "" {
|
|
129
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
130
|
+
return nil, err
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
m.newSession(opts)
|
|
134
|
+
return m, nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func (m *Manager) timestamp() string { return m.now().UTC().Format("2006-01-02T15:04:05.000Z") }
|
|
138
|
+
|
|
139
|
+
func (m *Manager) newSession(opts Options) {
|
|
140
|
+
m.sessionID = opts.ID
|
|
141
|
+
if m.sessionID == "" {
|
|
142
|
+
m.sessionID = uuidLike()
|
|
143
|
+
}
|
|
144
|
+
ts := m.timestamp()
|
|
145
|
+
header := Entry{"type": "session", "version": float64(CurrentVersion), "id": m.sessionID, "timestamp": ts, "cwd": m.cwd}
|
|
146
|
+
if opts.ParentSession != "" {
|
|
147
|
+
header["parentSession"] = opts.ParentSession
|
|
148
|
+
}
|
|
149
|
+
m.entries = []Entry{header}
|
|
150
|
+
m.byID = map[string]Entry{}
|
|
151
|
+
m.leafID = ""
|
|
152
|
+
m.flushed = false
|
|
153
|
+
if m.persist {
|
|
154
|
+
stamp := strings.NewReplacer(":", "-", ".", "-").Replace(ts)
|
|
155
|
+
m.file = filepath.Join(m.dir, stamp+"_"+m.sessionID+".jsonl")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Open loads an existing session file, appending to it from here on.
|
|
160
|
+
func Open(path string) (*Manager, error) {
|
|
161
|
+
entries, err := LoadEntries(path)
|
|
162
|
+
if err != nil {
|
|
163
|
+
return nil, err
|
|
164
|
+
}
|
|
165
|
+
if len(entries) == 0 {
|
|
166
|
+
return nil, fmt.Errorf("Session file is not a valid pi session: %s", path)
|
|
167
|
+
}
|
|
168
|
+
abs, _ := filepath.Abs(path)
|
|
169
|
+
m := &Manager{file: abs, dir: filepath.Dir(abs), persist: true, flushed: true, now: time.Now}
|
|
170
|
+
m.entries = entries
|
|
171
|
+
m.sessionID, _ = entries[0]["id"].(string)
|
|
172
|
+
m.cwd, _ = entries[0]["cwd"].(string)
|
|
173
|
+
m.buildIndex()
|
|
174
|
+
return m, nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// LoadEntries reads a session file: header first. Malformed lines are skipped; a file whose first
|
|
178
|
+
// line is not a session header yields nothing.
|
|
179
|
+
func LoadEntries(path string) ([]Entry, error) {
|
|
180
|
+
f, err := os.Open(path)
|
|
181
|
+
if err != nil {
|
|
182
|
+
if os.IsNotExist(err) {
|
|
183
|
+
return nil, nil
|
|
184
|
+
}
|
|
185
|
+
return nil, err
|
|
186
|
+
}
|
|
187
|
+
defer f.Close()
|
|
188
|
+
var entries []Entry
|
|
189
|
+
r := bufio.NewReaderSize(f, 1<<20)
|
|
190
|
+
for {
|
|
191
|
+
line, err := r.ReadString('\n')
|
|
192
|
+
if e, ok := ParseLine(line); ok {
|
|
193
|
+
entries = append(entries, e)
|
|
194
|
+
}
|
|
195
|
+
if err != nil {
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if len(entries) == 0 {
|
|
200
|
+
return nil, nil
|
|
201
|
+
}
|
|
202
|
+
if entries[0].Type() != "session" {
|
|
203
|
+
return nil, nil
|
|
204
|
+
}
|
|
205
|
+
if _, ok := entries[0]["id"].(string); !ok {
|
|
206
|
+
return nil, nil
|
|
207
|
+
}
|
|
208
|
+
migrate(entries)
|
|
209
|
+
return entries, nil
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// migrate brings entries of an older format up to the current version (in place).
|
|
213
|
+
func migrate(entries []Entry) {
|
|
214
|
+
version := 1.0
|
|
215
|
+
if v, ok := entries[0]["version"].(float64); ok {
|
|
216
|
+
version = v
|
|
217
|
+
}
|
|
218
|
+
if version >= CurrentVersion {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if version < 2 {
|
|
222
|
+
prev := ""
|
|
223
|
+
ids := map[string]bool{}
|
|
224
|
+
for i, e := range entries {
|
|
225
|
+
if e.Type() == "session" {
|
|
226
|
+
e["version"] = float64(2)
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
id := newID()[:8]
|
|
230
|
+
for ids[id] {
|
|
231
|
+
id = newID()[:8]
|
|
232
|
+
}
|
|
233
|
+
ids[id] = true
|
|
234
|
+
e["id"] = id
|
|
235
|
+
if prev == "" {
|
|
236
|
+
e["parentId"] = nil
|
|
237
|
+
} else {
|
|
238
|
+
e["parentId"] = prev
|
|
239
|
+
}
|
|
240
|
+
prev = id
|
|
241
|
+
if e.Type() == "compaction" {
|
|
242
|
+
if idx, ok := e["firstKeptEntryIndex"].(float64); ok {
|
|
243
|
+
if int(idx) >= 0 && int(idx) < len(entries) && entries[int(idx)].Type() != "session" {
|
|
244
|
+
e["firstKeptEntryId"] = entries[int(idx)]["id"]
|
|
245
|
+
}
|
|
246
|
+
delete(e, "firstKeptEntryIndex")
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
_ = i
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for _, e := range entries {
|
|
253
|
+
if e.Type() == "session" {
|
|
254
|
+
e["version"] = float64(CurrentVersion)
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
if msg := e.Message(); msg != nil && msg["role"] == "hookMessage" {
|
|
258
|
+
msg["role"] = "custom"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func (m *Manager) buildIndex() {
|
|
264
|
+
m.byID = map[string]Entry{}
|
|
265
|
+
m.leafID = ""
|
|
266
|
+
for _, e := range m.entries {
|
|
267
|
+
if e.Type() == "session" {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
m.byID[e.ID()] = e
|
|
271
|
+
m.leafID = e.ID()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// SessionID is the session's id.
|
|
276
|
+
func (m *Manager) SessionID() string { return m.sessionID }
|
|
277
|
+
|
|
278
|
+
// Cwd is the working directory the session was made for.
|
|
279
|
+
func (m *Manager) Cwd() string { return m.cwd }
|
|
280
|
+
|
|
281
|
+
// File is the session file path; "" for an in-memory session. It may not exist yet.
|
|
282
|
+
func (m *Manager) File() string { return m.file }
|
|
283
|
+
|
|
284
|
+
// Header is the session header.
|
|
285
|
+
func (m *Manager) Header() Entry {
|
|
286
|
+
m.mu.Lock()
|
|
287
|
+
defer m.mu.Unlock()
|
|
288
|
+
return m.entries[0]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func (m *Manager) hasAssistant() bool {
|
|
292
|
+
for _, e := range m.entries {
|
|
293
|
+
if e.Role() == "assistant" {
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func (m *Manager) writeAll() error {
|
|
301
|
+
f, err := os.OpenFile(m.file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
|
|
302
|
+
if err != nil {
|
|
303
|
+
return err
|
|
304
|
+
}
|
|
305
|
+
defer f.Close()
|
|
306
|
+
w := bufio.NewWriter(f)
|
|
307
|
+
for _, e := range m.entries {
|
|
308
|
+
if err := writeLine(w, e); err != nil {
|
|
309
|
+
return err
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return w.Flush()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func (m *Manager) appendLine(e Entry) error {
|
|
316
|
+
f, err := os.OpenFile(m.file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
|
|
317
|
+
if err != nil {
|
|
318
|
+
return err
|
|
319
|
+
}
|
|
320
|
+
defer f.Close()
|
|
321
|
+
w := bufio.NewWriter(f)
|
|
322
|
+
if err := writeLine(w, e); err != nil {
|
|
323
|
+
return err
|
|
324
|
+
}
|
|
325
|
+
return w.Flush()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func (m *Manager) persistEntry(e Entry) error {
|
|
329
|
+
if !m.persist || m.file == "" {
|
|
330
|
+
return nil
|
|
331
|
+
}
|
|
332
|
+
if !m.hasAssistant() {
|
|
333
|
+
if m.flushed {
|
|
334
|
+
return m.appendLine(e)
|
|
335
|
+
}
|
|
336
|
+
return nil
|
|
337
|
+
}
|
|
338
|
+
if !m.flushed {
|
|
339
|
+
if err := m.writeAll(); err != nil {
|
|
340
|
+
return err
|
|
341
|
+
}
|
|
342
|
+
m.flushed = true
|
|
343
|
+
return nil
|
|
344
|
+
}
|
|
345
|
+
return m.appendLine(e)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func (m *Manager) generateID() string {
|
|
349
|
+
for i := 0; i < 100; i++ {
|
|
350
|
+
id := newID()[:8]
|
|
351
|
+
if _, taken := m.byID[id]; !taken {
|
|
352
|
+
return id
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return uuidLike()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func (m *Manager) appendEntry(e Entry) (string, error) {
|
|
359
|
+
m.entries = append(m.entries, e)
|
|
360
|
+
m.byID[e.ID()] = e
|
|
361
|
+
m.leafID = e.ID()
|
|
362
|
+
return e.ID(), m.persistEntry(e)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func (m *Manager) child(typ string) Entry {
|
|
366
|
+
e := Entry{"type": typ, "id": m.generateID(), "timestamp": m.timestamp()}
|
|
367
|
+
if m.leafID == "" {
|
|
368
|
+
e["parentId"] = nil
|
|
369
|
+
} else {
|
|
370
|
+
e["parentId"] = m.leafID
|
|
371
|
+
}
|
|
372
|
+
return e
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// AppendMessage appends a message as a child of the leaf and returns its entry id.
|
|
376
|
+
func (m *Manager) AppendMessage(message map[string]any) (string, error) {
|
|
377
|
+
m.mu.Lock()
|
|
378
|
+
defer m.mu.Unlock()
|
|
379
|
+
e := m.child("message")
|
|
380
|
+
e["message"] = message
|
|
381
|
+
return m.appendEntry(e)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// AppendSessionInfo records the session's display name.
|
|
385
|
+
func (m *Manager) AppendSessionInfo(name string) (string, error) {
|
|
386
|
+
m.mu.Lock()
|
|
387
|
+
defer m.mu.Unlock()
|
|
388
|
+
e := m.child("session_info")
|
|
389
|
+
e["name"] = strings.TrimSpace(strings.NewReplacer("\r\n", " ", "\n", " ", "\r", " ").Replace(name))
|
|
390
|
+
return m.appendEntry(e)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// AppendCompaction records a compaction.
|
|
394
|
+
func (m *Manager) AppendCompaction(summary, firstKeptEntryID string, tokensBefore int) (string, error) {
|
|
395
|
+
m.mu.Lock()
|
|
396
|
+
defer m.mu.Unlock()
|
|
397
|
+
e := m.child("compaction")
|
|
398
|
+
e["summary"], e["firstKeptEntryId"], e["tokensBefore"] = summary, firstKeptEntryID, float64(tokensBefore)
|
|
399
|
+
return m.appendEntry(e)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Append appends an arbitrary entry of the given type as a child of the leaf; fields are merged in.
|
|
403
|
+
func (m *Manager) Append(typ string, fields map[string]any) (string, error) {
|
|
404
|
+
m.mu.Lock()
|
|
405
|
+
defer m.mu.Unlock()
|
|
406
|
+
e := m.child(typ)
|
|
407
|
+
for k, v := range fields {
|
|
408
|
+
e[k] = v
|
|
409
|
+
}
|
|
410
|
+
return m.appendEntry(e)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// SessionName is the latest session_info name, "" if none (an empty name clears it).
|
|
414
|
+
func (m *Manager) SessionName() string {
|
|
415
|
+
m.mu.Lock()
|
|
416
|
+
defer m.mu.Unlock()
|
|
417
|
+
for i := len(m.entries) - 1; i >= 0; i-- {
|
|
418
|
+
if m.entries[i].Type() == "session_info" {
|
|
419
|
+
s, _ := m.entries[i]["name"].(string)
|
|
420
|
+
return strings.TrimSpace(s)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return ""
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// LeafID is the current leaf ("" before any entry).
|
|
427
|
+
func (m *Manager) LeafID() string { m.mu.Lock(); defer m.mu.Unlock(); return m.leafID }
|
|
428
|
+
|
|
429
|
+
// LeafEntry is the current leaf entry, or nil.
|
|
430
|
+
func (m *Manager) LeafEntry() Entry {
|
|
431
|
+
m.mu.Lock()
|
|
432
|
+
defer m.mu.Unlock()
|
|
433
|
+
return m.byID[m.leafID]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Entry looks an entry up by id.
|
|
437
|
+
func (m *Manager) Entry(id string) (Entry, bool) {
|
|
438
|
+
m.mu.Lock()
|
|
439
|
+
defer m.mu.Unlock()
|
|
440
|
+
e, ok := m.byID[id]
|
|
441
|
+
return e, ok
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Entries are all entries except the header, in file order.
|
|
445
|
+
func (m *Manager) Entries() []Entry {
|
|
446
|
+
m.mu.Lock()
|
|
447
|
+
defer m.mu.Unlock()
|
|
448
|
+
return append([]Entry(nil), m.entries[1:]...)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Branch is the path from the root to fromID (the leaf when empty).
|
|
452
|
+
func (m *Manager) Branch(fromID string) []Entry {
|
|
453
|
+
m.mu.Lock()
|
|
454
|
+
defer m.mu.Unlock()
|
|
455
|
+
if fromID == "" {
|
|
456
|
+
fromID = m.leafID
|
|
457
|
+
}
|
|
458
|
+
return pathTo(m.byID, fromID)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
func pathTo(byID map[string]Entry, id string) []Entry {
|
|
462
|
+
var path []Entry
|
|
463
|
+
seen := map[string]bool{}
|
|
464
|
+
for cur, ok := byID[id]; ok && !seen[cur.ID()]; cur, ok = byID[cur.ParentID()] {
|
|
465
|
+
seen[cur.ID()] = true // a corrupt file must not loop forever
|
|
466
|
+
path = append(path, cur)
|
|
467
|
+
if cur.ParentID() == "" {
|
|
468
|
+
break
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
|
|
472
|
+
path[i], path[j] = path[j], path[i]
|
|
473
|
+
}
|
|
474
|
+
return path
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// SetLeaf moves the leaf to an existing entry ("" moves it before the first entry).
|
|
478
|
+
func (m *Manager) SetLeaf(id string) error {
|
|
479
|
+
m.mu.Lock()
|
|
480
|
+
defer m.mu.Unlock()
|
|
481
|
+
if id != "" {
|
|
482
|
+
if _, ok := m.byID[id]; !ok {
|
|
483
|
+
return fmt.Errorf("Entry %s not found", id)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
m.leafID = id
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ContextEntries is the compaction-aware entry list along the current branch.
|
|
491
|
+
func (m *Manager) ContextEntries() []Entry {
|
|
492
|
+
m.mu.Lock()
|
|
493
|
+
defer m.mu.Unlock()
|
|
494
|
+
return BuildContextEntries(m.entries[1:], m.leafID, m.byID)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// BuildContextEntries follows the branch to leafID (the last entry when leafID is empty and
|
|
498
|
+
// byID has no such entry). If the path holds compaction entries, the latest is represented by the
|
|
499
|
+
// compaction entry itself, followed by the kept entries starting at firstKeptEntryId and every
|
|
500
|
+
// entry after it; older summarised entries are omitted.
|
|
501
|
+
func BuildContextEntries(entries []Entry, leafID string, byID map[string]Entry) []Entry {
|
|
502
|
+
if byID == nil {
|
|
503
|
+
byID = map[string]Entry{}
|
|
504
|
+
for _, e := range entries {
|
|
505
|
+
byID[e.ID()] = e
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
leaf, ok := byID[leafID]
|
|
509
|
+
if !ok || leafID == "" {
|
|
510
|
+
if leafID == "" && len(entries) > 0 {
|
|
511
|
+
return nil // an explicit "before any entry" leaf
|
|
512
|
+
}
|
|
513
|
+
if len(entries) == 0 {
|
|
514
|
+
return nil
|
|
515
|
+
}
|
|
516
|
+
leaf = entries[len(entries)-1]
|
|
517
|
+
}
|
|
518
|
+
path := pathTo(byID, leaf.ID())
|
|
519
|
+
compactionIdx := -1
|
|
520
|
+
for i, e := range path {
|
|
521
|
+
if e.Type() == "compaction" {
|
|
522
|
+
compactionIdx = i
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if compactionIdx < 0 {
|
|
526
|
+
return path
|
|
527
|
+
}
|
|
528
|
+
compaction := path[compactionIdx]
|
|
529
|
+
out := []Entry{compaction}
|
|
530
|
+
found := false
|
|
531
|
+
kept, _ := compaction["firstKeptEntryId"].(string)
|
|
532
|
+
for _, e := range path[:compactionIdx] {
|
|
533
|
+
if e.ID() == kept {
|
|
534
|
+
found = true
|
|
535
|
+
}
|
|
536
|
+
if found && !(e.Type() == "message" && e.Role() == "system") {
|
|
537
|
+
out = append(out, e)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return append(out, path[compactionIdx+1:]...)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ContextSettings is the thinking level and model in effect at the leaf: the last
|
|
544
|
+
// thinking_level_change, and the last model_change or assistant message, along the branch.
|
|
545
|
+
func (m *Manager) ContextSettings() (thinkingLevel, provider, modelID string) {
|
|
546
|
+
thinkingLevel = "off"
|
|
547
|
+
for _, e := range m.Branch("") {
|
|
548
|
+
switch {
|
|
549
|
+
case e.Type() == "thinking_level_change":
|
|
550
|
+
thinkingLevel, _ = e["thinkingLevel"].(string)
|
|
551
|
+
case e.Type() == "model_change":
|
|
552
|
+
provider, _ = e["provider"].(string)
|
|
553
|
+
modelID, _ = e["modelId"].(string)
|
|
554
|
+
case e.Role() == "assistant":
|
|
555
|
+
provider, _ = e.Message()["provider"].(string)
|
|
556
|
+
modelID, _ = e.Message()["model"].(string)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return thinkingLevel, provider, modelID
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// FromEntries builds a read-only manager over entries obtained elsewhere (the host's session
|
|
563
|
+
// mirror), which carry no header: one is synthesised from id and cwd. The leaf is the last entry
|
|
564
|
+
// unless leafID names another. file is only reported by File.
|
|
565
|
+
func FromEntries(cwd, id, file string, entries []Entry, leafID string) (*Manager, error) {
|
|
566
|
+
timestamp := ""
|
|
567
|
+
if len(entries) > 0 {
|
|
568
|
+
timestamp = entries[0].Timestamp()
|
|
569
|
+
}
|
|
570
|
+
header := Entry{"type": "session", "version": CurrentVersion, "id": id, "timestamp": timestamp, "cwd": cwd}
|
|
571
|
+
m := &Manager{file: file, sessionID: id, cwd: cwd, now: time.Now, flushed: true}
|
|
572
|
+
m.entries = append([]Entry{header}, entries...)
|
|
573
|
+
m.buildIndex()
|
|
574
|
+
if leafID != "" {
|
|
575
|
+
if err := m.SetLeaf(leafID); err != nil {
|
|
576
|
+
return nil, err
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return m, nil
|
|
580
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package settings
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// Access is what the settings file lets remote clients do beyond talking to the session. Every
|
|
10
|
+
// capability is off unless the file turns it on; a file without these keys grants nothing.
|
|
11
|
+
type Access struct {
|
|
12
|
+
// AllowedOrigins are browser origins that may connect (a request without an Origin header,
|
|
13
|
+
// from a native client, is always allowed).
|
|
14
|
+
AllowedOrigins []string
|
|
15
|
+
// Filesystem: browse, read, write and watch files. Roots default to the session's working
|
|
16
|
+
// directory; Unrestricted lifts the confinement and must be stated.
|
|
17
|
+
Filesystem struct {
|
|
18
|
+
Enabled bool
|
|
19
|
+
Roots []string
|
|
20
|
+
Unrestricted bool
|
|
21
|
+
}
|
|
22
|
+
// Terminals: shells on this machine, claimed by the client that starts them.
|
|
23
|
+
Terminals struct{ Enabled bool }
|
|
24
|
+
// AllowSessionDeletion lets a client delete other sessions' files. The running session can
|
|
25
|
+
// never be deleted by a client.
|
|
26
|
+
AllowSessionDeletion bool
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// LoadAccess reads the optional capability keys of the settings file at path. The file must exist
|
|
30
|
+
// (LoadDirect creates it); keys of the wrong type are errors naming the file.
|
|
31
|
+
func LoadAccess(path string) (Access, error) {
|
|
32
|
+
raw, err := os.ReadFile(path)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return Access{}, err
|
|
35
|
+
}
|
|
36
|
+
var stored struct {
|
|
37
|
+
AllowedOrigins []string `json:"allowedOrigins"`
|
|
38
|
+
Filesystem *struct {
|
|
39
|
+
Enabled bool `json:"enabled"`
|
|
40
|
+
Roots []string `json:"roots"`
|
|
41
|
+
Unrestricted bool `json:"unrestricted"`
|
|
42
|
+
} `json:"filesystem"`
|
|
43
|
+
Terminals *struct {
|
|
44
|
+
Enabled bool `json:"enabled"`
|
|
45
|
+
} `json:"terminals"`
|
|
46
|
+
AllowSessionDeletion bool `json:"allowSessionDeletion"`
|
|
47
|
+
}
|
|
48
|
+
if err := json.Unmarshal(raw, &stored); err != nil {
|
|
49
|
+
return Access{}, &Error{path, fmt.Sprintf("a capability setting has the wrong type (%v)", err)}
|
|
50
|
+
}
|
|
51
|
+
var out Access
|
|
52
|
+
out.AllowedOrigins = stored.AllowedOrigins
|
|
53
|
+
if stored.Filesystem != nil {
|
|
54
|
+
out.Filesystem.Enabled = stored.Filesystem.Enabled
|
|
55
|
+
out.Filesystem.Roots = stored.Filesystem.Roots
|
|
56
|
+
out.Filesystem.Unrestricted = stored.Filesystem.Unrestricted
|
|
57
|
+
}
|
|
58
|
+
if stored.Terminals != nil {
|
|
59
|
+
out.Terminals.Enabled = stored.Terminals.Enabled
|
|
60
|
+
}
|
|
61
|
+
out.AllowSessionDeletion = stored.AllowSessionDeletion
|
|
62
|
+
return out, nil
|
|
63
|
+
}
|