p9: introduce s/ session directory; use nanosecond session IDs
Sessions are now nested under s/ instead of the root, making it straightforward to count active sessions and find newly created ones by offset: n=$(ls s/ | wc -l) echo "new agent=foo" > ctl sid=$(ls s/ | sort | sed -n "$((n+1))p") Session IDs are now Unix nanosecond timestamps (from ollie's updated NewSessionID), so ls s/ | sort gives strict creation order. The s/ directory is a stable listing surface; ctl remains at the root for control operations. All path parsing updated throughout: pathType, readDir, makeStat, handleWrite, readChat, readReply, readFile. README updated to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f97b9d355c
commit
d9a4ec3255
94
README.md
94
README.md
|
|
@ -9,17 +9,20 @@ The goal is integration, not self-sufficiency. Rather than providing orchestrati
|
||||||
```
|
```
|
||||||
ollie/
|
ollie/
|
||||||
ctl write: "new [backend=x] [model=x] [agent=x]" | "kill <session-id>"
|
ctl write: "new [backend=x] [model=x] [agent=x]" | "kill <session-id>"
|
||||||
<session-id>/
|
s/ dir: one entry per active session, sorted by creation time
|
||||||
prompt write: submit a prompt to the agent (clears reply)
|
<session-id>/
|
||||||
chat read: cumulative conversation history
|
prompt write: submit a prompt to the agent (clears reply)
|
||||||
reply read: assistant text from the most recent turn only
|
chat read: cumulative conversation history
|
||||||
state read: current agent state (idle, thinking, calling: <tool>)
|
reply read: assistant text from the most recent turn only
|
||||||
ctl write: stop | interrupt | /<slash-command>
|
state read: current agent state (idle, thinking, calling: <tool>)
|
||||||
backend r/w: active backend name
|
ctl write: stop | interrupt | /<slash-command>
|
||||||
agent r/w: active agent name
|
backend r/w: active backend name
|
||||||
model r/w: active model name
|
agent r/w: active agent name
|
||||||
|
model r/w: active model name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Session IDs are Unix nanosecond timestamps with a random suffix (e.g. `1744276689123456789-2b986c`), so `ls s/` sorted lexicographically gives creation order.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
@ -53,12 +56,12 @@ echo "new backend=ollama model=qwen3:8b agent=myagent" > ~/mnt/ollie/ctl
|
||||||
All options are optional and can be specified in any order. Unrecognised keys are rejected.
|
All options are optional and can be specified in any order. Unrecognised keys are rejected.
|
||||||
Valid keys: `backend`, `model`, `agent`.
|
Valid keys: `backend`, `model`, `agent`.
|
||||||
|
|
||||||
A new session directory appears under the mount point named by timestamp + random suffix (e.g. `20260410-014002-ba70fc`).
|
A new session directory appears under `s/`, named by Unix nanosecond timestamp + random suffix (e.g. `1744276689123456789-2b986c`).
|
||||||
|
|
||||||
### Send a prompt
|
### Send a prompt
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo "what files are in the current directory?" > ~/mnt/ollie/<session-id>/prompt
|
echo "what files are in the current directory?" > ~/mnt/ollie/s/<session-id>/prompt
|
||||||
```
|
```
|
||||||
|
|
||||||
Writes dispatch asynchronously on close, so the shell returns immediately. The agent runs in the background.
|
Writes dispatch asynchronously on close, so the shell returns immediately. The agent runs in the background.
|
||||||
|
|
@ -66,8 +69,8 @@ Writes dispatch asynchronously on close, so the shell returns immediately. The a
|
||||||
### Read the conversation
|
### Read the conversation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat ~/mnt/ollie/<session-id>/chat # full history snapshot
|
cat ~/mnt/ollie/s/<session-id>/chat # full history snapshot
|
||||||
tail -f ~/mnt/ollie/<session-id>/chat # follow output as it arrives
|
tail -f ~/mnt/ollie/s/<session-id>/chat # follow output as it arrives
|
||||||
```
|
```
|
||||||
|
|
||||||
The `chat` file is an append-only log of the full conversation. Format:
|
The `chat` file is an append-only log of the full conversation. Format:
|
||||||
|
|
@ -82,17 +85,17 @@ assistant: <response>
|
||||||
### Check agent state
|
### Check agent state
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat ~/mnt/ollie/<session-id>/state
|
cat ~/mnt/ollie/s/<session-id>/state
|
||||||
# idle | thinking | calling: <toolname>
|
# idle | thinking | calling: <toolname>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Control a session
|
### Control a session
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo stop > ~/mnt/ollie/<session-id>/ctl # interrupt the current turn
|
echo stop > ~/mnt/ollie/s/<session-id>/ctl # interrupt the current turn
|
||||||
echo /compact > ~/mnt/ollie/<session-id>/ctl # summarize context
|
echo /compact > ~/mnt/ollie/s/<session-id>/ctl # summarize context
|
||||||
echo /clear > ~/mnt/ollie/<session-id>/ctl # clear session history
|
echo /clear > ~/mnt/ollie/s/<session-id>/ctl # clear session history
|
||||||
echo /model qwen3:8b > ~/mnt/ollie/<session-id>/ctl
|
echo /model qwen3:8b > ~/mnt/ollie/s/<session-id>/ctl
|
||||||
```
|
```
|
||||||
|
|
||||||
`ctl` accepts `stop`/`interrupt` or any `/slash-command` supported by the agent. Arbitrary text is rejected.
|
`ctl` accepts `stop`/`interrupt` or any `/slash-command` supported by the agent. Arbitrary text is rejected.
|
||||||
|
|
@ -100,9 +103,9 @@ echo /model qwen3:8b > ~/mnt/ollie/<session-id>/ctl
|
||||||
### Switch backend, model, or agent
|
### Switch backend, model, or agent
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo ollama > ~/mnt/ollie/<session-id>/backend
|
echo ollama > ~/mnt/ollie/s/<session-id>/backend
|
||||||
echo qwen3:8b > ~/mnt/ollie/<session-id>/model
|
echo qwen3:8b > ~/mnt/ollie/s/<session-id>/model
|
||||||
echo myagent > ~/mnt/ollie/<session-id>/agent
|
echo myagent > ~/mnt/ollie/s/<session-id>/agent
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kill a session
|
### Kill a session
|
||||||
|
|
@ -147,41 +150,44 @@ Three sessions with specialized agent configs run a feedback loop. The reviewer
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cd ~/mnt/ollie
|
cd ~/mnt/ollie
|
||||||
|
|
||||||
|
# Count existing sessions so we can identify the newly created ones by offset.
|
||||||
|
n=$(ls s/ | wc -l)
|
||||||
|
|
||||||
echo "new agent=developer" > ctl
|
echo "new agent=developer" > ctl
|
||||||
echo "new agent=reviewer" > ctl
|
echo "new agent=reviewer" > ctl
|
||||||
echo "new agent=tester" > ctl
|
echo "new agent=tester" > ctl
|
||||||
|
|
||||||
dev=$(ls -d [0-9]* | sed -n '1p')
|
dev=$(ls s/ | sort | sed -n "$((n+1))p")
|
||||||
rev=$(ls -d [0-9]* | sed -n '2p')
|
rev=$(ls s/ | sort | sed -n "$((n+2))p")
|
||||||
tst=$(ls -d [0-9]* | sed -n '3p')
|
tst=$(ls s/ | sort | sed -n "$((n+3))p")
|
||||||
|
|
||||||
wait_reply() {
|
wait_reply() {
|
||||||
while [ "$(wc -c < $1/reply)" -eq 0 ]; do sleep 1; done
|
while [ "$(wc -c < s/$1/reply)" -eq 0 ]; do sleep 1; done
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "implement a function that parses a JSON config file" > $dev/prompt
|
echo "implement a function that parses a JSON config file" > s/$dev/prompt
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
wait_reply $dev
|
wait_reply $dev
|
||||||
code=$(cat $dev/reply)
|
code=$(cat s/$dev/reply)
|
||||||
|
|
||||||
# review phase
|
# review phase
|
||||||
printf "Review the following code. End your response with LGTM if it is ready, or PTAL if it needs revision.\n\n%s" "$code" > $rev/prompt
|
printf "Review the following code. End your response with LGTM if it is ready, or PTAL if it needs revision.\n\n%s" "$code" > s/$rev/prompt
|
||||||
wait_reply $rev
|
wait_reply $rev
|
||||||
|
|
||||||
if ! grep -qi "LGTM" $rev/reply; then
|
if ! grep -qi "LGTM" s/$rev/reply; then
|
||||||
{ echo "revise this code based on the feedback below."
|
{ echo "revise this code based on the feedback below."
|
||||||
echo "--- code ---"; echo "$code"
|
echo "--- code ---"; echo "$code"
|
||||||
echo "--- feedback ---"; cat $rev/reply
|
echo "--- feedback ---"; cat s/$rev/reply
|
||||||
} > $dev/prompt
|
} > s/$dev/prompt
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# test phase
|
# test phase
|
||||||
printf "Test the following code. End your response with Approved if all tests pass, or Rejected if they do not.\n\n%s" "$code" > $tst/prompt
|
printf "Test the following code. End your response with Approved if all tests pass, or Rejected if they do not.\n\n%s" "$code" > s/$tst/prompt
|
||||||
wait_reply $tst
|
wait_reply $tst
|
||||||
|
|
||||||
if grep -qi "Approved" $tst/reply; then
|
if grep -qi "Approved" s/$tst/reply; then
|
||||||
echo "done."
|
echo "done."
|
||||||
echo "$code"
|
echo "$code"
|
||||||
break
|
break
|
||||||
|
|
@ -189,8 +195,8 @@ while true; do
|
||||||
|
|
||||||
{ echo "fix the failures reported below."
|
{ echo "fix the failures reported below."
|
||||||
echo "--- code ---"; echo "$code"
|
echo "--- code ---"; echo "$code"
|
||||||
echo "--- test report ---"; cat $tst/reply
|
echo "--- test report ---"; cat s/$tst/reply
|
||||||
} > $dev/prompt
|
} > s/$dev/prompt
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -224,7 +230,7 @@ _spawn_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def session_ids():
|
def session_ids():
|
||||||
return {e.name for e in BASE.iterdir() if e.is_dir()}
|
return {e.name for e in (BASE / "s").iterdir() if e.is_dir()}
|
||||||
|
|
||||||
def spawn_session(agent="default"):
|
def spawn_session(agent="default"):
|
||||||
with _spawn_lock:
|
with _spawn_lock:
|
||||||
|
|
@ -240,7 +246,7 @@ def kill_session(sid):
|
||||||
(BASE / "ctl").write_text(f"kill {sid}\n")
|
(BASE / "ctl").write_text(f"kill {sid}\n")
|
||||||
|
|
||||||
def wait_reply(sid):
|
def wait_reply(sid):
|
||||||
path = BASE / sid / "reply"
|
path = BASE / "s" / sid / "reply"
|
||||||
while True:
|
while True:
|
||||||
if path.stat().st_size > 0:
|
if path.stat().st_size > 0:
|
||||||
return path.read_text().strip()
|
return path.read_text().strip()
|
||||||
|
|
@ -253,7 +259,7 @@ def run_subagent(spec):
|
||||||
sid = spawn_session(agent)
|
sid = spawn_session(agent)
|
||||||
try:
|
try:
|
||||||
prompt = f"{context}\n\n{task}".strip() if context else task
|
prompt = f"{context}\n\n{task}".strip() if context else task
|
||||||
(BASE / sid / "prompt").write_text(prompt)
|
(BASE / "s" / sid / "prompt").write_text(prompt)
|
||||||
return {"agent": agent, "reply": wait_reply(sid)}
|
return {"agent": agent, "reply": wait_reply(sid)}
|
||||||
finally:
|
finally:
|
||||||
kill_session(sid)
|
kill_session(sid)
|
||||||
|
|
@ -302,17 +308,17 @@ Since agents have access to `execute_code`, a session can write and execute a wo
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ echo new > ~/mnt/ollie/ctl
|
$ echo new > ~/mnt/ollie/ctl
|
||||||
$ ls ~/mnt/ollie/
|
$ ls ~/mnt/ollie/s/
|
||||||
20260410-014002-ba70fc ctl
|
1744276689123456789-2b986c
|
||||||
$ sid=20260410-014002-ba70fc
|
$ sid=1744276689123456789-2b986c
|
||||||
$ tail -f ~/mnt/ollie/$sid/chat &
|
$ tail -f ~/mnt/ollie/s/$sid/chat &
|
||||||
$ echo "list the go files in /home/lkn/src/ollie" > ~/mnt/ollie/$sid/prompt
|
$ echo "list the go files in /home/lkn/src/ollie" > ~/mnt/ollie/s/$sid/prompt
|
||||||
user: list the go files in /home/lkn/src/ollie
|
user: list the go files in /home/lkn/src/ollie
|
||||||
assistant: -> execute_code({"code":"find /home/lkn/src/ollie -name '*.go'","language":"bash"})
|
assistant: -> execute_code({"code":"find /home/lkn/src/ollie -name '*.go'","language":"bash"})
|
||||||
= pkg/agent/core.go
|
= pkg/agent/core.go
|
||||||
pkg/agent/loop.go
|
pkg/agent/loop.go
|
||||||
...
|
...
|
||||||
assistant: The Go source files are: core.go, loop.go, ...
|
assistant: The Go source files are: core.go, loop.go, ...
|
||||||
$ cat ~/mnt/ollie/$sid/state
|
$ cat ~/mnt/ollie/s/$sid/state
|
||||||
idle
|
idle
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,17 @@
|
||||||
// ctl (write) create/destroy sessions
|
// ctl (write) create/destroy sessions
|
||||||
// "new [backend] [agent]" creates a session
|
// "new [backend] [agent]" creates a session
|
||||||
// "kill <session-id>" destroys a session
|
// "kill <session-id>" destroys a session
|
||||||
// {session-id}/
|
// s/ (dir) session directory; one entry per active session,
|
||||||
// ctl (write) session control: compact, clear, interrupt
|
// sorted lexicographically by session ID (= creation
|
||||||
// prompt (write) submit a prompt to the agent
|
// order, since IDs are unix-nanosecond timestamps).
|
||||||
// chat (read) cumulative chat history; grows as conversation
|
// {session-id}/
|
||||||
|
// ctl (write) session control: compact, clear, interrupt
|
||||||
|
// prompt (write) submit a prompt to the agent
|
||||||
|
// chat (read) cumulative chat history; grows as conversation
|
||||||
// progresses — use cat for a snapshot, tail -f
|
// progresses — use cat for a snapshot, tail -f
|
||||||
// to follow new output.
|
// to follow new output.
|
||||||
// backend (r/w) read/write the active backend name
|
// backend (r/w) read/write the active backend name
|
||||||
// agent (r/w) read/write the active agent name
|
// agent (r/w) read/write the active agent name
|
||||||
package p9
|
package p9
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -185,26 +188,27 @@ func (s *Server) pathType(path string) string {
|
||||||
return "dir"
|
return "dir"
|
||||||
}
|
}
|
||||||
trimmed := strings.TrimPrefix(path, "/")
|
trimmed := strings.TrimPrefix(path, "/")
|
||||||
parts := strings.SplitN(trimmed, "/", 2)
|
parts := strings.SplitN(trimmed, "/", 3)
|
||||||
switch len(parts) {
|
switch {
|
||||||
case 1:
|
case len(parts) == 1 && parts[0] == "ctl":
|
||||||
if parts[0] == "ctl" {
|
return "file"
|
||||||
return "file"
|
case len(parts) == 1 && parts[0] == "s":
|
||||||
}
|
return "dir"
|
||||||
|
case len(parts) == 2 && parts[0] == "s":
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
_, ok := s.sessions[parts[0]]
|
_, ok := s.sessions[parts[1]]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return "dir"
|
return "dir"
|
||||||
}
|
}
|
||||||
case 2:
|
case len(parts) == 3 && parts[0] == "s":
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
_, ok := s.sessions[parts[0]]
|
_, ok := s.sessions[parts[1]]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch parts[1] {
|
switch parts[2] {
|
||||||
case "ctl", "prompt", "chat", "reply", "backend", "agent", "model", "state":
|
case "ctl", "prompt", "chat", "reply", "backend", "agent", "model", "state":
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
|
|
@ -353,12 +357,12 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
// Returns 0 bytes when offset is at or past the current end (normal EOF);
|
// Returns 0 bytes when offset is at or past the current end (normal EOF);
|
||||||
// tail -f detects growth via stat and re-reads from its last position.
|
// tail -f detects growth via stat and re-reads from its last position.
|
||||||
func (s *Server) readChat(fc *plan9.Fcall, path string) *plan9.Fcall {
|
func (s *Server) readChat(fc *plan9.Fcall, path string) *plan9.Fcall {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 2)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 3 || parts[0] != "s" {
|
||||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
sess := s.sessions[parts[0]]
|
sess := s.sessions[parts[1]]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if sess == nil {
|
if sess == nil {
|
||||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
||||||
|
|
@ -382,12 +386,12 @@ func (s *Server) readChat(fc *plan9.Fcall, path string) *plan9.Fcall {
|
||||||
|
|
||||||
// readReply serves bytes from the session's reply buffer at the requested offset.
|
// readReply serves bytes from the session's reply buffer at the requested offset.
|
||||||
func (s *Server) readReply(fc *plan9.Fcall, path string) *plan9.Fcall {
|
func (s *Server) readReply(fc *plan9.Fcall, path string) *plan9.Fcall {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 2)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 3 || parts[0] != "s" {
|
||||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
sess := s.sessions[parts[0]]
|
sess := s.sessions[parts[1]]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if sess == nil {
|
if sess == nil {
|
||||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
||||||
|
|
@ -411,11 +415,11 @@ func (s *Server) readReply(fc *plan9.Fcall, path string) *plan9.Fcall {
|
||||||
|
|
||||||
// readFile returns the text content for a readable session file.
|
// readFile returns the text content for a readable session file.
|
||||||
func (s *Server) readFile(path string) string {
|
func (s *Server) readFile(path string) string {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 2)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 3 || parts[0] != "s" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
sessID, fileName := parts[0], parts[1]
|
sessID, fileName := parts[1], parts[2]
|
||||||
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
sess := s.sessions[sessID]
|
sess := s.sessions[sessID]
|
||||||
|
|
@ -501,11 +505,12 @@ func (s *Server) handleWrite(path, input string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 2)
|
// Path format: /s/{sessid}/{file}
|
||||||
if len(parts) != 2 {
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
|
if len(parts) != 3 || parts[0] != "s" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sessID, fileName := parts[0], parts[1]
|
sessID, fileName := parts[1], parts[2]
|
||||||
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
sess := s.sessions[sessID]
|
sess := s.sessions[sessID]
|
||||||
|
|
@ -795,13 +800,15 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
|
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
dirs = append(dirs, makeDir("ctl", "/ctl", false, 0200))
|
dirs = append(dirs, makeDir("ctl", "/ctl", false, 0200))
|
||||||
|
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
||||||
|
} else if path == "/s" {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
for id := range s.sessions {
|
for id := range s.sessions {
|
||||||
dirs = append(dirs, makeDir(id, "/"+id, true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir(id, "/s/"+id, true, plan9.DMDIR|0555))
|
||||||
}
|
}
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
} else {
|
} else {
|
||||||
// Session directory.
|
// Session directory: path is /s/{sessid}
|
||||||
sessPath := path
|
sessPath := path
|
||||||
type entry struct {
|
type entry struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -892,13 +899,14 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
Muid: "ollie",
|
Muid: "ollie",
|
||||||
}
|
}
|
||||||
|
|
||||||
// For chat files, report the actual log size and Qid version so that
|
// For chat/reply files, report actual size and Qid version so that
|
||||||
// polling tools (tail -f) can detect growth via stat.
|
// polling tools (tail -f) can detect growth via stat.
|
||||||
|
// Path format: /s/{sessid}/{file}
|
||||||
if base == "chat" || base == "reply" {
|
if base == "chat" || base == "reply" {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 2)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 3 && parts[0] == "s" {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
sess := s.sessions[parts[0]]
|
sess := s.sessions[parts[1]]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
sess.mu.RLock()
|
sess.mu.RLock()
|
||||||
|
|
|
||||||
Reference in New Issue