forge-openclaw-plugin 0.3.9 → 0.3.11
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/README.md +1 -1
- package/dist/companion-iroh-src/src/lib.rs +328 -69
- package/dist/companion-iroh-src/src/main.rs +15 -2
- package/dist/server/server/src/app.js +11 -4
- package/dist/server/server/src/health.js +11 -76
- package/dist/server/server/src/services/companion-iroh.js +14 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/forge-openclaw/SKILL.md +35 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +107 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +61 -0
package/README.md
CHANGED
|
@@ -561,7 +561,7 @@ If you explicitly want the old laptop-driven publish path, run the same script w
|
|
|
561
561
|
`FORGE_RELEASE_MODE=prepare` and it will still publish directly after pushing.
|
|
562
562
|
|
|
563
563
|
For the exact prerequisites, tags, and GitHub secret names, use
|
|
564
|
-
`docs/release-cheat-sheet.md`.
|
|
564
|
+
`docs/release/release-cheat-sheet.md`.
|
|
565
565
|
|
|
566
566
|
ClawHub note:
|
|
567
567
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
1
2
|
use std::ffi::{CStr, CString, c_char};
|
|
2
|
-
use std::
|
|
3
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
4
|
+
use std::time::{Duration, Instant};
|
|
3
5
|
|
|
6
|
+
use iroh::endpoint::Connection;
|
|
4
7
|
use iroh::{Endpoint, SecretKey};
|
|
5
8
|
use protocol::{
|
|
6
9
|
BridgeRequest, BridgeResponse, COMPANION_ALPN, FORGE_AGENT_NAME, ForgeHttpRequest,
|
|
@@ -12,6 +15,129 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
|
12
15
|
pub mod protocol;
|
|
13
16
|
|
|
14
17
|
const MAX_FRAME_BYTES: usize = 50 * 1024 * 1024;
|
|
18
|
+
const IROH_CLIENT_TIMING_HEADER: &str = "x-forge-iroh-client-timing-ms";
|
|
19
|
+
const IROH_CLIENT_CONNECTION_REUSED_HEADER: &str = "x-forge-iroh-client-connection-reused";
|
|
20
|
+
const FFI_RUNTIME_WORKER_THREADS: usize = 6;
|
|
21
|
+
#[cfg(test)]
|
|
22
|
+
const FOREGROUND_HEALTH_SYNC_STREAMS: usize = 6;
|
|
23
|
+
|
|
24
|
+
struct FfiIrohState {
|
|
25
|
+
runtime: tokio::runtime::Runtime,
|
|
26
|
+
endpoint: Mutex<Option<Arc<Endpoint>>>,
|
|
27
|
+
connections: Mutex<HashMap<ConnectionCacheKey, Arc<Connection>>>,
|
|
28
|
+
secret_key: SecretKey,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static FFI_STATE: OnceLock<Result<FfiIrohState, String>> = OnceLock::new();
|
|
32
|
+
|
|
33
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
34
|
+
struct ConnectionCacheKey {
|
|
35
|
+
node_id: String,
|
|
36
|
+
relay: Option<String>,
|
|
37
|
+
token: String,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl ConnectionCacheKey {
|
|
41
|
+
fn from_payload(payload: &PairPayload) -> Self {
|
|
42
|
+
Self {
|
|
43
|
+
node_id: payload.node_id.clone(),
|
|
44
|
+
relay: payload.relay.clone(),
|
|
45
|
+
token: payload.token.clone(),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn ffi_state() -> Result<&'static FfiIrohState, String> {
|
|
51
|
+
match FFI_STATE.get_or_init(FfiIrohState::new) {
|
|
52
|
+
Ok(state) => Ok(state),
|
|
53
|
+
Err(error) => Err(error.clone()),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl FfiIrohState {
|
|
58
|
+
fn new() -> Result<Self, String> {
|
|
59
|
+
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
60
|
+
.worker_threads(FFI_RUNTIME_WORKER_THREADS)
|
|
61
|
+
.enable_all()
|
|
62
|
+
.build()
|
|
63
|
+
.map_err(|error| format!("building Forge Iroh runtime: {error}"))?;
|
|
64
|
+
Ok(Self {
|
|
65
|
+
runtime,
|
|
66
|
+
endpoint: Mutex::new(None),
|
|
67
|
+
connections: Mutex::new(HashMap::new()),
|
|
68
|
+
secret_key: SecretKey::generate(),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async fn cached_endpoint(&self) -> Result<Arc<Endpoint>, String> {
|
|
73
|
+
if let Some(endpoint) = self.endpoint.lock().map_err(lock_error)?.as_ref().cloned() {
|
|
74
|
+
return Ok(endpoint);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let endpoint = Arc::new(
|
|
78
|
+
Endpoint::builder(iroh::endpoint::presets::N0)
|
|
79
|
+
.secret_key(self.secret_key.clone())
|
|
80
|
+
.bind()
|
|
81
|
+
.await
|
|
82
|
+
.map_err(|error| format!("binding Iroh endpoint: {error}"))?,
|
|
83
|
+
);
|
|
84
|
+
let mut guard = self.endpoint.lock().map_err(lock_error)?;
|
|
85
|
+
if let Some(existing) = guard.as_ref().cloned() {
|
|
86
|
+
return Ok(existing);
|
|
87
|
+
}
|
|
88
|
+
*guard = Some(endpoint.clone());
|
|
89
|
+
Ok(endpoint)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async fn cached_connection(
|
|
93
|
+
&self,
|
|
94
|
+
payload: &PairPayload,
|
|
95
|
+
) -> Result<(ConnectionCacheKey, Arc<Connection>, bool), String> {
|
|
96
|
+
let key = ConnectionCacheKey::from_payload(payload);
|
|
97
|
+
if let Some(connection) = self
|
|
98
|
+
.connections
|
|
99
|
+
.lock()
|
|
100
|
+
.map_err(lock_error)?
|
|
101
|
+
.get(&key)
|
|
102
|
+
.cloned()
|
|
103
|
+
{
|
|
104
|
+
return Ok((key, connection, true));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let endpoint = self.cached_endpoint().await?;
|
|
108
|
+
let connection = Arc::new(connect_iroh(endpoint, payload).await?);
|
|
109
|
+
let mut guard = self.connections.lock().map_err(lock_error)?;
|
|
110
|
+
if let Some(existing) = guard.get(&key).cloned() {
|
|
111
|
+
return Ok((key, existing, true));
|
|
112
|
+
}
|
|
113
|
+
guard.insert(key.clone(), connection.clone());
|
|
114
|
+
Ok((key, connection, false))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn evict_connection(&self, key: &ConnectionCacheKey, connection: &Arc<Connection>) {
|
|
118
|
+
let Ok(mut guard) = self.connections.lock() else {
|
|
119
|
+
connection.close(
|
|
120
|
+
iroh::endpoint::VarInt::from_u32(1),
|
|
121
|
+
b"forge connection cache lock failed",
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
};
|
|
125
|
+
if guard
|
|
126
|
+
.get(key)
|
|
127
|
+
.is_some_and(|existing| Arc::ptr_eq(existing, connection))
|
|
128
|
+
{
|
|
129
|
+
guard.remove(key);
|
|
130
|
+
connection.close(
|
|
131
|
+
iroh::endpoint::VarInt::from_u32(1),
|
|
132
|
+
b"forge request stream failed",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn lock_error<T>(error: std::sync::PoisonError<T>) -> String {
|
|
139
|
+
format!("Forge Iroh client state lock poisoned: {error}")
|
|
140
|
+
}
|
|
15
141
|
|
|
16
142
|
#[derive(Debug, Deserialize)]
|
|
17
143
|
#[serde(rename_all = "camelCase")]
|
|
@@ -87,12 +213,9 @@ fn into_c_string(value: String) -> *mut c_char {
|
|
|
87
213
|
}
|
|
88
214
|
|
|
89
215
|
fn run_ffi_http_request(request: FfiHttpRequest) -> Result<FfiHttpResponse, String> {
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.map_err(|error| format!("building Forge Iroh runtime: {error}"))?;
|
|
94
|
-
runtime.block_on(async move {
|
|
95
|
-
let response = send_http_request_over_iroh(request).await?;
|
|
216
|
+
let state = ffi_state()?;
|
|
217
|
+
state.runtime.block_on(async move {
|
|
218
|
+
let response = send_http_request_over_iroh(state, request).await?;
|
|
96
219
|
Ok(FfiHttpResponse {
|
|
97
220
|
ok: true,
|
|
98
221
|
status: Some(response.status),
|
|
@@ -103,74 +226,141 @@ fn run_ffi_http_request(request: FfiHttpRequest) -> Result<FfiHttpResponse, Stri
|
|
|
103
226
|
})
|
|
104
227
|
}
|
|
105
228
|
|
|
106
|
-
async fn send_http_request_over_iroh(
|
|
229
|
+
async fn send_http_request_over_iroh(
|
|
230
|
+
state: &FfiIrohState,
|
|
231
|
+
request: FfiHttpRequest,
|
|
232
|
+
) -> Result<ForgeHttpResponse, String> {
|
|
107
233
|
validate_pair_payload(&request.pair_payload)?;
|
|
108
234
|
validate_proxy_path(&request.path)?;
|
|
109
235
|
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
);
|
|
236
|
+
let total_started_at = Instant::now();
|
|
237
|
+
let connection_started_at = Instant::now();
|
|
238
|
+
let (cache_key, conn, connection_reused) =
|
|
239
|
+
state.cached_connection(&request.pair_payload).await?;
|
|
240
|
+
let connection_ms = elapsed_ms(connection_started_at);
|
|
241
|
+
match send_request_over_connection(&conn, request).await {
|
|
242
|
+
Ok((mut response, mut timing)) => {
|
|
243
|
+
timing.connection_ms = connection_ms;
|
|
244
|
+
timing.connection_reused = connection_reused;
|
|
245
|
+
timing.total_ms = elapsed_ms(total_started_at);
|
|
246
|
+
response.headers.push(HeaderPair {
|
|
247
|
+
name: IROH_CLIENT_TIMING_HEADER.to_string(),
|
|
248
|
+
value: timing.to_header_value(),
|
|
249
|
+
});
|
|
250
|
+
response.headers.push(HeaderPair {
|
|
251
|
+
name: IROH_CLIENT_CONNECTION_REUSED_HEADER.to_string(),
|
|
252
|
+
value: if connection_reused { "1" } else { "0" }.to_string(),
|
|
253
|
+
});
|
|
254
|
+
Ok(response)
|
|
128
255
|
}
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
&ForgeHttpRequest {
|
|
151
|
-
v: PROTOCOL_VERSION,
|
|
152
|
-
method: request.method,
|
|
153
|
-
path: request.path,
|
|
154
|
-
headers: request.headers,
|
|
155
|
-
body_base64: request.body_base64,
|
|
156
|
-
},
|
|
157
|
-
)
|
|
158
|
-
.await?;
|
|
159
|
-
let response = tokio::time::timeout(
|
|
160
|
-
Duration::from_secs(60),
|
|
161
|
-
read_json_frame::<ForgeHttpResponse, _>(&mut recv),
|
|
162
|
-
)
|
|
163
|
-
.await
|
|
164
|
-
.map_err(|_| "timed out waiting for Forge Iroh response".to_string())??;
|
|
165
|
-
conn.close(
|
|
166
|
-
iroh::endpoint::VarInt::from_u32(0),
|
|
167
|
-
b"forge request complete",
|
|
256
|
+
Err(error) => {
|
|
257
|
+
state.evict_connection(&cache_key, &conn);
|
|
258
|
+
Err(error)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async fn connect_iroh(
|
|
264
|
+
endpoint: Arc<Endpoint>,
|
|
265
|
+
payload: &PairPayload,
|
|
266
|
+
) -> Result<Connection, String> {
|
|
267
|
+
let node_id = payload
|
|
268
|
+
.node_id
|
|
269
|
+
.parse()
|
|
270
|
+
.map_err(|error| format!("parsing Iroh node id: {error}"))?;
|
|
271
|
+
let mut addr = iroh::EndpointAddr::new(node_id);
|
|
272
|
+
if let Some(relay) = payload.relay.as_deref() {
|
|
273
|
+
addr = addr.with_relay_url(
|
|
274
|
+
relay
|
|
275
|
+
.parse()
|
|
276
|
+
.map_err(|error| format!("parsing Iroh relay URL: {error}"))?,
|
|
168
277
|
);
|
|
169
|
-
Ok(response)
|
|
170
278
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
279
|
+
endpoint
|
|
280
|
+
.connect(addr, COMPANION_ALPN)
|
|
281
|
+
.await
|
|
282
|
+
.map_err(|error| format!("connecting over Forge Iroh bridge: {error}"))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async fn send_request_over_connection(
|
|
286
|
+
conn: &Connection,
|
|
287
|
+
request: FfiHttpRequest,
|
|
288
|
+
) -> Result<(ForgeHttpResponse, IrohClientTiming), String> {
|
|
289
|
+
let mut timing = IrohClientTiming::default();
|
|
290
|
+
let open_started_at = Instant::now();
|
|
291
|
+
let (mut send, mut recv) = conn
|
|
292
|
+
.open_bi()
|
|
293
|
+
.await
|
|
294
|
+
.map_err(|error| format!("opening Iroh stream: {error}"))?;
|
|
295
|
+
timing.open_stream_ms = elapsed_ms(open_started_at);
|
|
296
|
+
|
|
297
|
+
let bridge_started_at = Instant::now();
|
|
298
|
+
write_json_frame(
|
|
299
|
+
&mut send,
|
|
300
|
+
&BridgeRequest::Connect {
|
|
301
|
+
v: PROTOCOL_VERSION,
|
|
302
|
+
token: request.pair_payload.token.clone(),
|
|
303
|
+
agent: FORGE_AGENT_NAME.to_string(),
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
.await?;
|
|
307
|
+
let ack: BridgeResponse = read_json_frame(&mut recv).await?;
|
|
308
|
+
validate_bridge_response(&ack)?;
|
|
309
|
+
timing.bridge_ack_ms = elapsed_ms(bridge_started_at);
|
|
310
|
+
|
|
311
|
+
let write_started_at = Instant::now();
|
|
312
|
+
write_json_frame(
|
|
313
|
+
&mut send,
|
|
314
|
+
&ForgeHttpRequest {
|
|
315
|
+
v: PROTOCOL_VERSION,
|
|
316
|
+
method: request.method,
|
|
317
|
+
path: request.path,
|
|
318
|
+
headers: request.headers,
|
|
319
|
+
body_base64: request.body_base64,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
.await?;
|
|
323
|
+
timing.write_request_ms = elapsed_ms(write_started_at);
|
|
324
|
+
|
|
325
|
+
let response_started_at = Instant::now();
|
|
326
|
+
let response = tokio::time::timeout(
|
|
327
|
+
Duration::from_secs(60),
|
|
328
|
+
read_json_frame::<ForgeHttpResponse, _>(&mut recv),
|
|
329
|
+
)
|
|
330
|
+
.await
|
|
331
|
+
.map_err(|_| "timed out waiting for Forge Iroh response".to_string())??;
|
|
332
|
+
timing.response_wait_ms = elapsed_ms(response_started_at);
|
|
333
|
+
Ok((response, timing))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[derive(Debug, Default)]
|
|
337
|
+
struct IrohClientTiming {
|
|
338
|
+
connection_reused: bool,
|
|
339
|
+
connection_ms: u128,
|
|
340
|
+
open_stream_ms: u128,
|
|
341
|
+
bridge_ack_ms: u128,
|
|
342
|
+
write_request_ms: u128,
|
|
343
|
+
response_wait_ms: u128,
|
|
344
|
+
total_ms: u128,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
impl IrohClientTiming {
|
|
348
|
+
fn to_header_value(&self) -> String {
|
|
349
|
+
format!(
|
|
350
|
+
"reused={},total={},connection={},openStream={},bridgeAck={},writeRequest={},responseWait={}",
|
|
351
|
+
if self.connection_reused { 1 } else { 0 },
|
|
352
|
+
self.total_ms,
|
|
353
|
+
self.connection_ms,
|
|
354
|
+
self.open_stream_ms,
|
|
355
|
+
self.bridge_ack_ms,
|
|
356
|
+
self.write_request_ms,
|
|
357
|
+
self.response_wait_ms
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn elapsed_ms(started_at: Instant) -> u128 {
|
|
363
|
+
started_at.elapsed().as_millis()
|
|
174
364
|
}
|
|
175
365
|
|
|
176
366
|
fn validate_pair_payload(payload: &PairPayload) -> Result<(), String> {
|
|
@@ -276,4 +466,73 @@ mod tests {
|
|
|
276
466
|
fn proxy_path_rejects_absolute_urls() {
|
|
277
467
|
assert!(validate_proxy_path("https://example.com/api/v1/health").is_err());
|
|
278
468
|
}
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn ffi_reuses_runtime_and_endpoint_between_requests() {
|
|
472
|
+
let state = ffi_state().expect("ffi state");
|
|
473
|
+
let first = state
|
|
474
|
+
.runtime
|
|
475
|
+
.block_on(state.cached_endpoint())
|
|
476
|
+
.expect("first endpoint");
|
|
477
|
+
let second = state
|
|
478
|
+
.runtime
|
|
479
|
+
.block_on(state.cached_endpoint())
|
|
480
|
+
.expect("second endpoint");
|
|
481
|
+
|
|
482
|
+
assert!(Arc::ptr_eq(&first, &second));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#[test]
|
|
486
|
+
fn connection_cache_key_tracks_remote_and_token() {
|
|
487
|
+
let payload = PairPayload {
|
|
488
|
+
v: PROTOCOL_VERSION,
|
|
489
|
+
node_id: "node-a".to_string(),
|
|
490
|
+
token: "token-a".to_string(),
|
|
491
|
+
host_name: Some("host".to_string()),
|
|
492
|
+
relay: Some("https://relay.example".to_string()),
|
|
493
|
+
};
|
|
494
|
+
let same = PairPayload {
|
|
495
|
+
host_name: None,
|
|
496
|
+
..payload.clone()
|
|
497
|
+
};
|
|
498
|
+
let different_token = PairPayload {
|
|
499
|
+
token: "token-b".to_string(),
|
|
500
|
+
..payload.clone()
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
assert_eq!(
|
|
504
|
+
ConnectionCacheKey::from_payload(&payload),
|
|
505
|
+
ConnectionCacheKey::from_payload(&same)
|
|
506
|
+
);
|
|
507
|
+
assert_ne!(
|
|
508
|
+
ConnectionCacheKey::from_payload(&payload),
|
|
509
|
+
ConnectionCacheKey::from_payload(&different_token)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#[test]
|
|
514
|
+
fn iroh_client_timing_header_names_response_wait() {
|
|
515
|
+
let timing = IrohClientTiming {
|
|
516
|
+
connection_reused: true,
|
|
517
|
+
connection_ms: 1,
|
|
518
|
+
open_stream_ms: 2,
|
|
519
|
+
bridge_ack_ms: 3,
|
|
520
|
+
write_request_ms: 4,
|
|
521
|
+
response_wait_ms: 5,
|
|
522
|
+
total_ms: 15,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
assert_eq!(
|
|
526
|
+
timing.to_header_value(),
|
|
527
|
+
"reused=1,total=15,connection=1,openStream=2,bridgeAck=3,writeRequest=4,responseWait=5"
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#[test]
|
|
532
|
+
fn ffi_runtime_keeps_up_with_foreground_health_sync_window() {
|
|
533
|
+
assert!(
|
|
534
|
+
FFI_RUNTIME_WORKER_THREADS >= FOREGROUND_HEALTH_SYNC_STREAMS,
|
|
535
|
+
"Iroh FFI runtime workers should not be narrower than foreground HealthKit streams"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
279
538
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use std::path::{Path, PathBuf};
|
|
2
|
-
use std::time::Duration;
|
|
2
|
+
use std::time::{Duration, Instant};
|
|
3
3
|
|
|
4
4
|
use anyhow::{Context, anyhow};
|
|
5
5
|
use base64::Engine;
|
|
@@ -16,6 +16,7 @@ use tracing::{info, warn};
|
|
|
16
16
|
use tracing_subscriber::EnvFilter;
|
|
17
17
|
|
|
18
18
|
const MAX_FRAME_BYTES: usize = 50 * 1024 * 1024;
|
|
19
|
+
const IROH_HOST_TIMING_HEADER: &str = "x-forge-iroh-host-timing-ms";
|
|
19
20
|
|
|
20
21
|
#[derive(Parser, Debug)]
|
|
21
22
|
#[command(version, about = "Forge Companion transport over Iroh QUIC")]
|
|
@@ -276,9 +277,12 @@ async fn proxy_http_request(
|
|
|
276
277
|
.context("decoding request body")?;
|
|
277
278
|
builder = builder.body(bytes);
|
|
278
279
|
}
|
|
280
|
+
let request_started_at = Instant::now();
|
|
279
281
|
let response = builder.send().await.context("forwarding HTTP request")?;
|
|
282
|
+
let upstream_response_ms = request_started_at.elapsed().as_millis();
|
|
280
283
|
let status = response.status().as_u16();
|
|
281
|
-
let
|
|
284
|
+
let body_started_at = Instant::now();
|
|
285
|
+
let mut headers: Vec<HeaderPair> = response
|
|
282
286
|
.headers()
|
|
283
287
|
.iter()
|
|
284
288
|
.filter_map(|(name, value)| {
|
|
@@ -292,6 +296,15 @@ async fn proxy_http_request(
|
|
|
292
296
|
.bytes()
|
|
293
297
|
.await
|
|
294
298
|
.context("reading HTTP response body")?;
|
|
299
|
+
let body_read_ms = body_started_at.elapsed().as_millis();
|
|
300
|
+
let total_ms = request_started_at.elapsed().as_millis();
|
|
301
|
+
headers.push(HeaderPair {
|
|
302
|
+
name: IROH_HOST_TIMING_HEADER.to_string(),
|
|
303
|
+
value: format!(
|
|
304
|
+
"total={},upstreamResponse={},bodyRead={}",
|
|
305
|
+
total_ms, upstream_response_ms, body_read_ms
|
|
306
|
+
),
|
|
307
|
+
});
|
|
295
308
|
Ok(ForgeHttpResponse {
|
|
296
309
|
v: PROTOCOL_VERSION,
|
|
297
310
|
status,
|
|
@@ -5455,16 +5455,18 @@ function buildAgentOnboardingPayload(request) {
|
|
|
5455
5455
|
saveSuggestionTone: "gentle_optional",
|
|
5456
5456
|
maxQuestionsPerTurn: 1,
|
|
5457
5457
|
psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, make the next question help the user feel more able to name the experience rather than more examined, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. After one concrete example is clear and a hypothesis lands or is corrected, translate it into a saveable record shape such as a belief sentence, functional loop, behavior, mode, trigger report, value, event type, or emotion definition; ask one accuracy question instead of reopening broad exploration, then use the shared batch entity routes after the user accepts the wording or explicitly asks to save. When the user is updating a Psyche record because of one fresh episode, anchor in that episode before renaming the durable formulation, begin with the smallest part of the old wording that no longer fits, and do not reopen the full origin story unless the new understanding is truly structural. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
|
|
5458
|
-
|
|
5458
|
+
progressiveDisclosureRule: "Treat partial answers as progress, not as failed intake. Before asking another question, identify what is already usable: operation, entity or surface, target record or time span, working wording, owner or placement, route lane, and consent. Say the usable part back briefly, then ask only for the first missing detail that changes the action: duplicate disambiguation, hierarchy parent, time window, weekday, flow, run, node, correction, link, or save consent. For normal batch entities, do not ask for optional tags, priority, status, dates, color, links, or assignees when the accepted wording and meaningful body are already enough unless that metadata changes accountability, retrieval, or execution. For Movement, Life Force, and Workbench, if the user's wording already implies the dedicated lane, skip the broad route-family question and ask only for the target span, place, weekday, profile field, flow, run, node, output, correction, or consent that is still missing. For direct Psyche saves or updates, treat an offered belief sentence, functional loop, part voice, trigger episode, value phrase, event kind, emotion signature, or flashcard message as real data; ask one accuracy or consent question instead of reopening origin, evidence, or repair.",
|
|
5459
|
+
writeConfirmationRule: "After create, update, delete, restore, run, read, or repair actions, confirm the user-facing record, action, and result in the user's language instead of reopening intake. For batch creates and updates, confirm the working title or accepted wording, container, and owner or placement only when those changed retrieval, accountability, or execution; if optional tags, priority, status, color, links, dates, or assignees were left provisional, say that plainly once instead of asking for all of them. For action workflows, confirm the real product action such as task run started or completed, work adjustment applied, preference judgment or signal submitted, questionnaire run updated or completed, calendar connection synced, or self-observation note written. For Psyche saves, confirm the accepted wording and whether it was saved as a first version, update, link, archive, or distinct version; do not reopen origin, evidence, repair, or adjacent entity mapping after the save unless that next object is already visible and materially useful. Ask a follow-up only if it changes the next action: correction, link, schedule, run, publish, enrichment, preservation choice, or UI handoff.",
|
|
5460
|
+
specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the job first, then choose the dedicated route family internally and do not guess at a generic CRUD path. Use specializedDomainSurfaces.routeSelectionQuestions when they are present so the next follow-up selects the right route instead of asking generic questions. When available, use forge_call_movement_route, forge_call_life_force_route, or forge_call_workbench_route after the lane is clear. In user-facing language, talk about timeline, overlay, weekday template, published output, run detail, or node result rather than surfaces, payloads, read paths, mutation paths, or CRUD. If the truth of the current state is still uncertain, read the relevant dedicated view before you mutate it. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. After a concrete Movement, Life Force, or Workbench correction, mutation, or result-producing run, read the relevant view back when the user is trying to understand the result rather than just store it: timeline or place/settings detail for Movement, the Life Force overview for energy-planning impact, and flow detail, run detail, node result, latest node output, published output, or run history for Workbench. The canonical runtime routes stay under /api/v1/*, and the OpenClaw HTTP mirror exposes the same families under /forge/v1/movement, /forge/v1/life-force, and /forge/v1/workbench.",
|
|
5459
5461
|
reviewShortcutRule: "When the user is reviewing or correcting an existing record, ask what practical question they want the read or correction to answer, then narrow the saved object, timeframe, or route family first. Use the correct read posture before asking write-shaped questions: shared batch search or read hints for normal entities, wiki/calendar dedicated reads for specialized CRUD, read-model routes for overviews, and Movement, Life Force, or Workbench dedicated reads for those domain surfaces. After the read, answer the practical question before asking for any save, correction, link, run, enrichment, or publish detail. Do not reopen the whole intake unless the user is actually redefining the record.",
|
|
5460
5462
|
readModelWriteRule: "Self-observation is note-backed and should be written through observed notes with frontmatter.observedAt only when a lightweight episode observation is the right container. Do not use it as the default bucket for Psyche material: prefer trigger_report for one emotionally meaningful episode, behavior_pattern for functional analysis of a recurring loop, behavior for one repeated move, belief_entry for a core sentence, mode_guide_session or mode_profile for a central part-state, and wiki_page for durable memory such as books, articles, concepts, sources, or personal manuals. Sleep and workout sessions stay on batch CRUD by default; use the reflective review helpers only when enriching one already-known record after review.",
|
|
5461
5463
|
psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
|
|
5462
|
-
psycheHypothesisRule: "When one concrete Psyche example is visible, a helpful hypothesis should start from evidence in the user's own example, offer one testable interpretation, name the function without blame such as protection, prediction, relief, or cost, and ask whether the danger, need, or wording fits. Use the hypothesis timing checkpoint before asking a second or third deepening question: offer a hypothesis when one concrete episode, body cue, belief sentence, behavior, or mode voice is visible and the hypothesis would change the record shape, wording, links, or next action. Do not keep asking broad exploratory Psyche questions after the cue, meaning, protection, payoff, or cost is already visible. For behavior_pattern, belief_entry, mode_profile, mode_guide_session, and trigger_report, the next helpful move is usually one active formulation plus one correction question, not another passive reflection. Do not hypothesize yet when no concrete moment is visible, the user only wants a direct mechanical save, the user is flooded or unsafe, or the only available interpretation would be diagnosis-like, an origin story, or a certainty claim. Do not present schema, mode, belief, or pattern language as a verdict. If the user corrects the hypothesis, revise it once and move toward the saveable record shape instead of asking for another broad story.",
|
|
5464
|
+
psycheHypothesisRule: "When one concrete Psyche example is visible, a helpful hypothesis should start from evidence in the user's own example, offer one testable interpretation, name the function without blame such as protection, prediction, relief, or cost, and ask whether the danger, need, or wording fits. Use the hypothesis timing checkpoint before asking a second or third deepening question: offer a hypothesis when one concrete episode, body cue, belief sentence, behavior, or mode voice is visible and the hypothesis would change the record shape, wording, links, or next action. Do not keep asking broad exploratory Psyche questions after the cue, meaning, protection, payoff, or cost is already visible. For behavior_pattern, belief_entry, mode_profile, mode_guide_session, and trigger_report, the next helpful move is usually one active formulation plus one correction question, not another passive reflection. Hypotheses should reduce the formulation burden. Do not make the user prove the experience: after one hypothesis, ask one fit-or-correction question rather than a stack of evidence, origin, and repair questions. If accuracy needs grounding, ask for the smallest lived cue or contrast that would change the wording, danger, protection, payoff, cost, or record shape. Do not hypothesize yet when no concrete moment is visible, the user only wants a direct mechanical save, the user is flooded or unsafe, or the only available interpretation would be diagnosis-like, an origin story, or a certainty claim. Do not present schema, mode, belief, or pattern language as a verdict. If the user corrects the hypothesis, revise it once and move toward the saveable record shape instead of asking for another broad story.",
|
|
5463
5465
|
mixedIntentSequencingRule: "When one user message combines several Forge jobs, identify the primary job and the order of operations before asking a follow-up. If a read changes the truth of a later write, read first: Movement timeline or box detail before correction, Workbench run or node detail before editing or publishing, and Life Force overview before changing durable assumptions when the current energy picture is uncertain. If the user asks to understand and save Psyche material plus create a support record, formulate the primary Psyche record first, then derive the flashcard, note, link, task, or habit from the accepted wording. If the user already gave the concrete action, do not ask a broad lane question; say the product sequence briefly and ask only for the missing span, wording, flow, run, node, weekday, or link that changes the next action.",
|
|
5464
5466
|
duplicateDisambiguationRule: "Before creating or updating a normal stored entity when duplicate risk is plausible, search the shared batch entity route by entity type, distinctive title or wording, owner scope, and linked content. If a likely existing record appears, ask whether the user wants to update that record, link to it, or save a separate new record; do not reopen the whole create flow. For Psyche records, a similar belief, pattern, mode, trigger report, value, or flashcard is a formulation choice, not a duplicate error: compare the sentence, cue/payoff/cost, protective job, episode, urge sentence, or message and let the user choose update, link, or new version. For wiki_page and calendar_connection, use dedicated search/list/read routes before creating another page or connection. For Movement, Life Force, and Workbench, use the dedicated read lanes instead of batch duplicate search.",
|
|
5465
5467
|
destructiveActionRule: "Before deleting, archiving, invalidating, overwriting, disconnecting, or substantially replacing a Forge record or specialized object, confirm the exact target and what should remain understandable. Prefer normal soft-delete for stored entities unless the user explicitly asks for permanent removal. For Psyche records, preserve therapeutic history by asking whether the old belief, pattern, mode, trigger report, value, or flashcard should be updated, linked as history, archived, or kept distinct; do not delete it just because a cleaner formulation exists. For Movement, distinguish user-defined overlay deletion from automatic-box invalidation and stay/trip/point deletion, and read the specific span first when the target is uncertain. For calendar connections, Workbench flows, wiki pages, and questionnaire instruments, ask what downstream sync, published output, backlinks, run history, or completed runs should remain understandable before deleting or replacing the saved object.",
|
|
5466
5468
|
followUpQuestionRule: "After a substantive answer, do not restart the opener or jump to the next schema field. First say what became clearer in concrete language, then choose exactly one next lane: wording, boundary, placement, timing, route scope, link, hypothesis, or write confirmation. Ask the smallest question that would change the record shape, route choice, useful wording, timing, or links. If nothing decision-relevant would change, stop asking, summarize the working record, and act with consent.",
|
|
5467
|
-
antiDriftRule: "Avoid vague reflective filler and internal route language. Replace phrases like 'that sounds important' with the specific stake you heard, and replace API nouns like surface, CRUD, payload, mutation path, or endpoint with user-facing product nouns such as belief, pattern, note, wiki page, timeline, overlay, weekday template, flow, run, node result, or published output. If a question would only decorate the intake, skip it.",
|
|
5469
|
+
antiDriftRule: "Avoid vague reflective filler and internal route language. Keep a private action trace: intent, entity or dedicated domain lane, exact read/write/run tool, required target identifiers, and the one missing detail that would change the action. Do not narrate that trace to the user. Replace phrases like 'that sounds important' with the specific stake you heard, and replace API nouns like surface, CRUD, payload, mutation path, route key, or endpoint with user-facing product nouns such as belief, pattern, note, wiki page, timeline, overlay, missing stay, weekday template, flow, run, node result, or published output. Ask one product-language question when the trace is unclear; with the user, ask about the real thing: the span, place, weekday, flow, run, node, belief sentence, parent record, or save confirmation. When reporting actions, say the product result first: saved the belief, corrected the missing stay, updated the weekday energy pattern, or read the failed node. Mention route keys, HTTP paths, payloads, or batch routes only for implementation debugging. If a question would only decorate the intake, skip it.",
|
|
5468
5470
|
duplicateCheckRoute: "/api/v1/entities/search",
|
|
5469
5471
|
uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
|
|
5470
5472
|
browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job. Batch CRUD is the default for simple entities; avoid spamming the agent with a large one-route-per-entity mental model.",
|
|
@@ -8440,9 +8442,14 @@ export async function buildServer(options = {}) {
|
|
|
8440
8442
|
});
|
|
8441
8443
|
app.post("/api/v1/mobile/healthkit/sync-sessions/:id/chunks", { bodyLimit: 40_000_000 }, async (request) => {
|
|
8442
8444
|
const { id } = request.params;
|
|
8445
|
+
const startedAt = performance.now();
|
|
8443
8446
|
const rawPayloadJson = JSON.stringify((request.body ?? {}).payload ?? {});
|
|
8447
|
+
const chunk = ingestMobileHealthSyncChunk(id, mobileHealthSyncChunkSchema.parse(request.body ?? {}), rawPayloadJson);
|
|
8444
8448
|
return {
|
|
8445
|
-
chunk:
|
|
8449
|
+
chunk: {
|
|
8450
|
+
...chunk,
|
|
8451
|
+
serverProcessingMs: Math.max(0, Math.round(performance.now() - startedAt))
|
|
8452
|
+
}
|
|
8446
8453
|
};
|
|
8447
8454
|
});
|
|
8448
8455
|
app.post("/api/v1/mobile/healthkit/sync-sessions/:id/complete", async (request) => {
|
|
@@ -3347,78 +3347,6 @@ function applyWorkoutChunkImmediately(session, family, workouts) {
|
|
|
3347
3347
|
}), workouts.length, createdCount, updatedCount, mergedCount, now, now, runId);
|
|
3348
3348
|
});
|
|
3349
3349
|
}
|
|
3350
|
-
function mobileSyncWorkoutRowsByExternalUid(userId, externalUids) {
|
|
3351
|
-
const uniqueExternalUids = [...new Set(externalUids.filter(Boolean))];
|
|
3352
|
-
const rowsByExternalUid = new Map();
|
|
3353
|
-
const chunkSize = 500;
|
|
3354
|
-
for (let lowerBound = 0; lowerBound < uniqueExternalUids.length; lowerBound += chunkSize) {
|
|
3355
|
-
const chunk = uniqueExternalUids.slice(lowerBound, lowerBound + chunkSize);
|
|
3356
|
-
if (chunk.length === 0) {
|
|
3357
|
-
continue;
|
|
3358
|
-
}
|
|
3359
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
3360
|
-
const rows = getDatabase()
|
|
3361
|
-
.prepare(`SELECT *
|
|
3362
|
-
FROM health_workout_sessions
|
|
3363
|
-
WHERE user_id = ?
|
|
3364
|
-
AND source = 'apple_health'
|
|
3365
|
-
AND external_uid IN (${placeholders})`)
|
|
3366
|
-
.all(userId, ...chunk);
|
|
3367
|
-
for (const row of rows) {
|
|
3368
|
-
rowsByExternalUid.set(row.external_uid, row);
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
return rowsByExternalUid;
|
|
3372
|
-
}
|
|
3373
|
-
function applyWorkoutEvidenceChunkImmediately(session, payload) {
|
|
3374
|
-
const timeSeries = payload.workoutTimeSeries ?? [];
|
|
3375
|
-
const routes = payload.workoutRoutes ?? [];
|
|
3376
|
-
if (timeSeries.length === 0 && routes.length === 0) {
|
|
3377
|
-
return;
|
|
3378
|
-
}
|
|
3379
|
-
const pairing = mobileSyncSessionPairing(session);
|
|
3380
|
-
runInTransaction(() => {
|
|
3381
|
-
const rowsToRecompute = new Map();
|
|
3382
|
-
const rowsByExternalUid = mobileSyncWorkoutRowsByExternalUid(pairing.user_id, [
|
|
3383
|
-
...timeSeries
|
|
3384
|
-
.filter((entry) => entry.samples.length > 0)
|
|
3385
|
-
.map((entry) => entry.externalUid),
|
|
3386
|
-
...routes
|
|
3387
|
-
.filter((entry) => entry.routePoints.length > 0)
|
|
3388
|
-
.map((entry) => entry.externalUid)
|
|
3389
|
-
]);
|
|
3390
|
-
for (const entry of timeSeries) {
|
|
3391
|
-
const row = rowsByExternalUid.get(entry.externalUid);
|
|
3392
|
-
if (!row || entry.samples.length === 0) {
|
|
3393
|
-
continue;
|
|
3394
|
-
}
|
|
3395
|
-
upsertWorkoutTimeSeries({
|
|
3396
|
-
workoutId: row.id,
|
|
3397
|
-
userId: pairing.user_id,
|
|
3398
|
-
samples: entry.samples
|
|
3399
|
-
});
|
|
3400
|
-
rowsToRecompute.set(row.id, row);
|
|
3401
|
-
}
|
|
3402
|
-
for (const entry of routes) {
|
|
3403
|
-
const row = rowsByExternalUid.get(entry.externalUid);
|
|
3404
|
-
if (!row || entry.routePoints.length === 0) {
|
|
3405
|
-
continue;
|
|
3406
|
-
}
|
|
3407
|
-
upsertWorkoutRoutePoints({
|
|
3408
|
-
workoutId: row.id,
|
|
3409
|
-
userId: pairing.user_id,
|
|
3410
|
-
points: entry.routePoints
|
|
3411
|
-
});
|
|
3412
|
-
rowsToRecompute.set(row.id, row);
|
|
3413
|
-
}
|
|
3414
|
-
for (const row of rowsToRecompute.values()) {
|
|
3415
|
-
recomputeAndStoreWorkoutAnalytics(row);
|
|
3416
|
-
}
|
|
3417
|
-
for (const dateKeyValue of new Set([...rowsToRecompute.values()].map((row) => dayKey(row.started_at)))) {
|
|
3418
|
-
summarizeUserHealthDay(pairing.user_id, dateKeyValue);
|
|
3419
|
-
}
|
|
3420
|
-
});
|
|
3421
|
-
}
|
|
3422
3350
|
function markMobileHealthSyncChunkApplied(input) {
|
|
3423
3351
|
const now = nowIso();
|
|
3424
3352
|
getDatabase()
|
|
@@ -3474,8 +3402,10 @@ function applyMobileHealthSyncChunkImmediately(session, family, payload) {
|
|
|
3474
3402
|
return "workout_progressive_apply";
|
|
3475
3403
|
case "workout_time_series":
|
|
3476
3404
|
case "workout_routes":
|
|
3477
|
-
|
|
3478
|
-
|
|
3405
|
+
// Evidence chunks can be very large. Applying them here made the phone wait
|
|
3406
|
+
// for route/sample upserts plus analytics recomputation before each chunk
|
|
3407
|
+
// response. Store them quickly and apply once during completion instead.
|
|
3408
|
+
return null;
|
|
3479
3409
|
case "movement":
|
|
3480
3410
|
return applyMovementChunkImmediately(session, payload)
|
|
3481
3411
|
? "movement_progressive_apply"
|
|
@@ -3733,8 +3663,10 @@ function mergeMobileHealthSyncChunks(session, chunks, options = {}) {
|
|
|
3733
3663
|
const workoutsByExternalUid = new Map();
|
|
3734
3664
|
const tombstones = [];
|
|
3735
3665
|
for (const chunk of chunks.sort((left, right) => left.sequence - right.sequence)) {
|
|
3666
|
+
const keepWorkoutSummaryForDeferredEvidence = chunk.family === "workout_summaries" || chunk.family === "workout_archive";
|
|
3736
3667
|
const skipRecords = options.skipImmediatelyApplied === true &&
|
|
3737
|
-
mobileHealthSyncChunkWasImmediatelyApplied(chunk)
|
|
3668
|
+
mobileHealthSyncChunkWasImmediatelyApplied(chunk) &&
|
|
3669
|
+
keepWorkoutSummaryForDeferredEvidence === false;
|
|
3738
3670
|
if (skipRecords) {
|
|
3739
3671
|
continue;
|
|
3740
3672
|
}
|
|
@@ -3967,7 +3899,10 @@ function listMobileSyncCompletionChunks(syncSessionId) {
|
|
|
3967
3899
|
.prepare(`SELECT id, payload_json
|
|
3968
3900
|
FROM health_mobile_sync_chunks
|
|
3969
3901
|
WHERE sync_session_id = ?
|
|
3970
|
-
AND
|
|
3902
|
+
AND (
|
|
3903
|
+
applied_at IS NULL
|
|
3904
|
+
OR family IN ('workout_summaries', 'workout_archive')
|
|
3905
|
+
)`)
|
|
3971
3906
|
.all(syncSessionId);
|
|
3972
3907
|
if (payloadRows.length === 0) {
|
|
3973
3908
|
return chunks;
|
|
@@ -23,6 +23,16 @@ export async function buildCompanionPairingTransport(input) {
|
|
|
23
23
|
"Manual HTTP/TCP pairing was explicitly requested."
|
|
24
24
|
]);
|
|
25
25
|
}
|
|
26
|
+
if (selectedFallback) {
|
|
27
|
+
const fallbackMode = fallbackModeFor(selectedFallback.apiBaseUrl, input.fallbackMode);
|
|
28
|
+
return manualHttpTransport(selectedFallback.apiBaseUrl, selectedFallback.uiBaseUrl, [
|
|
29
|
+
fallbackMode === "tailscale"
|
|
30
|
+
? "Tailscale HTTPS pairing was selected as the primary companion transport."
|
|
31
|
+
: "A phone-reachable direct companion URL was selected as the primary transport.",
|
|
32
|
+
"Forge did not include Iroh transport metadata because direct pairing is active.",
|
|
33
|
+
"Run pairing again without a phone-facing public URL if you want an Iroh pairing instead."
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
26
36
|
if (!shouldAutoStartIrohHost()) {
|
|
27
37
|
return manualHttpTransport(requestApiBaseUrl, requestUiBaseUrl, [
|
|
28
38
|
"Forge Iroh companion transport is unavailable in this runtime, so Forge fell back to direct HTTP."
|
|
@@ -34,18 +44,14 @@ export async function buildCompanionPairingTransport(input) {
|
|
|
34
44
|
pairPayload: snapshot.pairPayload,
|
|
35
45
|
alpn: snapshot.alpn ?? COMPANION_IROH_ALPN,
|
|
36
46
|
localBaseUrl: snapshot.localBaseUrl,
|
|
37
|
-
fallbackApiBaseUrl:
|
|
38
|
-
fallbackUiBaseUrl:
|
|
39
|
-
fallbackMode:
|
|
40
|
-
? fallbackModeFor(selectedFallback.apiBaseUrl, input.fallbackMode)
|
|
41
|
-
: "none",
|
|
47
|
+
fallbackApiBaseUrl: null,
|
|
48
|
+
fallbackUiBaseUrl: null,
|
|
49
|
+
fallbackMode: "none",
|
|
42
50
|
recreateCommand: snapshot.recreateCommand ?? undefined,
|
|
43
51
|
startedAt: snapshot.startedAt ?? undefined,
|
|
44
52
|
notes: [
|
|
45
53
|
"Default pairing uses Forge's Rust Iroh transport over QUIC first.",
|
|
46
|
-
|
|
47
|
-
? "The QR includes the selected direct URL only as an explicit fallback/direct path."
|
|
48
|
-
: "No direct HTTP fallback was selected for this QR.",
|
|
54
|
+
"No direct HTTP fallback was selected for this QR.",
|
|
49
55
|
"The QR payload carries the Iroh node id, host token, optional relay, and ALPN forge-companion/1.",
|
|
50
56
|
"Manual HTTP/TCP pairing remains available with --manual-http for advanced local setups."
|
|
51
57
|
]
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "forge-openclaw-plugin",
|
|
3
3
|
"name": "Forge",
|
|
4
4
|
"description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.11",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -70,6 +70,14 @@ stored-entity CRUD, an action workflow, specialized CRUD, or a specialized domai
|
|
|
70
70
|
surface. Name the path plainly enough that another agent could follow it without
|
|
71
71
|
guessing.
|
|
72
72
|
|
|
73
|
+
Keep that route plan internal unless the user asks for implementation detail. Track
|
|
74
|
+
the intent, entity or dedicated domain lane, exact tool or route key, target
|
|
75
|
+
identifiers, and one missing detail privately; with the user, ask about the real
|
|
76
|
+
thing: the span, place, weekday, flow, run, node, belief sentence, parent record, or
|
|
77
|
+
save confirmation. Report product actions such as "saved the belief", "corrected the
|
|
78
|
+
missing stay", "updated the weekday energy pattern", or "read the failed node" before
|
|
79
|
+
any route-key or endpoint detail.
|
|
80
|
+
|
|
73
81
|
- Batch CRUD is the default for normal stored entities, including `goal`, `project`,
|
|
74
82
|
`strategy`, `task`, `habit`, `tag`, `note`, `insight`, `calendar_event`,
|
|
75
83
|
`work_block_template`, `task_timebox`, all main Psyche records, basic Preferences
|
|
@@ -243,6 +251,14 @@ Entity conversation rule:
|
|
|
243
251
|
time scope, and any ownership/placement that changes later use are already clear,
|
|
244
252
|
summarize once and write, read, run, or update instead of collecting optional
|
|
245
253
|
fields.
|
|
254
|
+
- Treat partial answers as progress. Before another follow-up, identify what is
|
|
255
|
+
already usable: operation, entity or surface, target record or time span, working
|
|
256
|
+
wording, owner or placement, route lane, and consent. Ask only for the first missing
|
|
257
|
+
detail that changes the action: duplicate disambiguation, hierarchy parent, time
|
|
258
|
+
window, weekday, flow, run, node, correction, link, or save consent.
|
|
259
|
+
- Do not ask for optional tags, priority, status, dates, color, links, or assignees
|
|
260
|
+
when accepted wording and meaningful body are enough unless that metadata changes
|
|
261
|
+
accountability, retrieval, or execution.
|
|
246
262
|
- Let each question have one job. Know what you are trying to clarify before you ask it.
|
|
247
263
|
- Before you ask, decide the exact missing thing you need and how that answer will help you name, place, or save the record.
|
|
248
264
|
- Prefer a progression of:
|
|
@@ -271,6 +287,19 @@ Entity conversation rule:
|
|
|
271
287
|
Movement, Life Force, or Workbench dedicated reads for those domain surfaces. After
|
|
272
288
|
the read, answer the practical question before asking for any save, correction,
|
|
273
289
|
link, run, enrichment, or publish detail.
|
|
290
|
+
- After create, update, delete, restore, run, read, or repair actions, confirm the
|
|
291
|
+
user-facing record, action, and result in the user's language instead of reopening
|
|
292
|
+
intake. For batch creates and updates, confirm the working title or accepted wording,
|
|
293
|
+
container, and owner or placement only when those changed retrieval, accountability,
|
|
294
|
+
or execution; if optional tags, priority, status, color, links, dates, or assignees
|
|
295
|
+
were left provisional, say that plainly once. For action workflows, confirm the real
|
|
296
|
+
product action: task run started or completed, work adjustment applied, preference
|
|
297
|
+
judgment or signal submitted, questionnaire run updated or completed, calendar
|
|
298
|
+
connection synced, or self-observation note written. For Psyche saves, confirm the
|
|
299
|
+
accepted wording and whether it was saved as a first version, update, link, archive,
|
|
300
|
+
or distinct version; do not reopen origin, evidence, repair, or adjacent entity
|
|
301
|
+
mapping after the save unless that next object is already visible and materially
|
|
302
|
+
useful.
|
|
274
303
|
- When updating an entity, start with what is changing, what should stay true, and what prompted the update now.
|
|
275
304
|
- When enough is clear, briefly summarize what you heard in the user's own language before asking for the last missing structural detail.
|
|
276
305
|
- Treat `userId` and human/bot assignees as accountability and scope, not as opening form fields. Ask whose human or bot record it is only when ownership changes visibility, review scope, collaboration, automation behavior, or later filtering; for read requests, ask user scope only when the answer would differ across owners.
|
|
@@ -300,6 +329,7 @@ Psyche interview rule:
|
|
|
300
329
|
- After the first real answer, choose one follow-up lane at a time: situation, sequence, meaning, protection, cost, longing/value, or tentative name.
|
|
301
330
|
- Do not minimize functional analysis, trigger chains, behavior patterns, modes, beliefs, or schema themes. Once at least one concrete example is clear, offer one careful interpretive hypothesis when it would help the user understand the function, protection, cost, belief, mode, or schema theme.
|
|
302
331
|
- Phrase interpretive hypotheses as collaborative and testable, not as verdicts. A good hypothesis says what the reaction may be protecting, predicting, relieving, or costing, then asks whether that lands or needs correction.
|
|
332
|
+
- For Psyche hypotheses, reduce the formulation burden. After one concrete example, offer one tentative function, danger, protection, payoff, or cost hypothesis and ask one fit-or-correction question. Do not make the user prove the experience, list evidence, or design repair before the wording feels held.
|
|
303
333
|
- Do not keep asking broad exploratory Psyche questions after the cue, meaning, protection, payoff, or cost is already visible. For `behavior_pattern`, `belief_entry`, `mode_profile`, `mode_guide_session`, and `trigger_report`, the next helpful move is usually one active formulation plus one correction question, not another passive reflection.
|
|
304
334
|
- Use the hypothesis timing checkpoint before asking a second or third deepening question: offer a hypothesis when one concrete episode, body cue, belief sentence, behavior, or mode voice is visible and the hypothesis would change the record shape, wording, links, or next action. Do not hypothesize yet when no concrete moment is visible, the user only wants a direct mechanical save, the user is flooded or unsafe, or the only available interpretation would be diagnosis-like, an origin story, or a certainty claim.
|
|
305
335
|
- If several Psyche containers are plausible, do not ask the user to choose from a taxonomy menu first. Reflect the lived difference, offer one careful hypothesis when a concrete example is visible, then distinguish the options in plain language: one episode as a `trigger_report`, a recurring loop as a `behavior_pattern`, one repeated move as `behavior`, one sentence as `belief_entry`, a part-state as `mode_profile` or `mode_guide_session`, or reusable future-labeling as `event_type` or `emotion_definition`.
|
|
@@ -320,6 +350,10 @@ Psyche interview rule:
|
|
|
320
350
|
- If the user already offers a usable belief sentence, value phrase, or mode name,
|
|
321
351
|
refine from their wording first instead of replacing it with a cleaner label too
|
|
322
352
|
early.
|
|
353
|
+
- If the user already offers a usable belief sentence, functional loop, part voice,
|
|
354
|
+
trigger episode, value phrase, event kind, emotion signature, or flashcard message,
|
|
355
|
+
treat it as real data and ask one accuracy or consent question instead of reopening
|
|
356
|
+
origin, evidence, or repair.
|
|
323
357
|
- When the conversation reveals an adjacent entity such as a linked belief, mode, value, pattern, or note, name that gently and ask whether the user wants to map it too.
|
|
324
358
|
- If nuance matters, preserve it in a linked Markdown `note` instead of forcing every detail into normalized fields.
|
|
325
359
|
- If the user shows imminent risk of self-harm, suicide, violence, inability to stay safe, or severe disorientation, stop normal intake and prioritize urgent human support or emergency help instead.
|
|
@@ -621,7 +655,7 @@ through `forge_create_entities` or `forge_update_entities`.
|
|
|
621
655
|
into a new run, note, or generic entity update unless the user asks for that.
|
|
622
656
|
- If you are unsure which specialized route family applies, check `forge_get_agent_onboarding` and use its `entityRouteModel.specializedDomainSurfaces` section before guessing.
|
|
623
657
|
- If the truth of the current Movement, Life Force, or Workbench state is still unclear, prefer the dedicated read before the mutation so the correction stays truthful.
|
|
624
|
-
- After a concrete Movement, Life Force, or Workbench correction, read the relevant specialized view back when the user is trying to understand the result rather than only store it.
|
|
658
|
+
- After a concrete Movement, Life Force, or Workbench correction, mutation, or result-producing run, read the relevant specialized view back when the user is trying to understand the result rather than only store it: timeline or place/settings detail for Movement, the Life Force overview for energy-planning impact, and flow detail, run detail, node result, latest node output, published output, or run history for Workbench.
|
|
625
659
|
|
|
626
660
|
Use live work tools for `task_run`:
|
|
627
661
|
`forge_log_work`, `forge_start_task_run`, `forge_heartbeat_task_run`, `forge_focus_task_run`, `forge_complete_task_run`, `forge_release_task_run`
|
|
@@ -101,6 +101,10 @@ Forge correctly, and gather only the structure that still matters.
|
|
|
101
101
|
opener. Move straight to the next missing clarification.
|
|
102
102
|
- After a substantive answer, briefly say what is becoming clear so the user can
|
|
103
103
|
correct the direction early.
|
|
104
|
+
- Treat partial answers as progress. Before asking again, mark what the user already
|
|
105
|
+
supplied: the operation, container, target record or span, working wording, route
|
|
106
|
+
lane, placement, and consent. Ask only for the first missing detail that would
|
|
107
|
+
change the save, read, run, correction, or link.
|
|
104
108
|
- Once the record is clear enough to name, stop exploring broadly and ask only for the
|
|
105
109
|
last missing structural detail.
|
|
106
110
|
- When the record is already clear enough to save, save it instead of performing a
|
|
@@ -120,6 +124,24 @@ With the user, say the human thing:
|
|
|
120
124
|
The API path still matters, but it should not leak into the question unless the user
|
|
121
125
|
is explicitly asking about implementation.
|
|
122
126
|
|
|
127
|
+
## Internal action trace, external wording
|
|
128
|
+
|
|
129
|
+
Before you ask or act, keep a private action trace: intent, entity or dedicated
|
|
130
|
+
domain lane, exact read/write/run tool, required target identifiers, and the one
|
|
131
|
+
missing detail that would change the action. Do not narrate that trace to the user.
|
|
132
|
+
|
|
133
|
+
- If the trace is clear, ask the user only for the missing real-world detail:
|
|
134
|
+
which span, place, weekday, flow, run, node, belief sentence, parent record, or
|
|
135
|
+
save confirmation.
|
|
136
|
+
- If the trace is not clear, ask one product-language question that resolves it
|
|
137
|
+
instead of presenting API options.
|
|
138
|
+
- When you report what you did, say the product action first: saved the belief,
|
|
139
|
+
corrected the missing stay, updated the weekday energy pattern, read the failed
|
|
140
|
+
node, or published the flow output. Mention route keys, HTTP paths, payloads, or
|
|
141
|
+
batch routes only for implementation debugging.
|
|
142
|
+
- This is especially important after mixed-intent requests. The user should feel a
|
|
143
|
+
coherent sequence, not see your internal routing table.
|
|
144
|
+
|
|
123
145
|
## Dedicated surface lane translation
|
|
124
146
|
|
|
125
147
|
Use this when Movement, Life Force, or Workbench work needs a route choice. The route
|
|
@@ -138,6 +160,28 @@ choice is an internal classification step, not a user-facing menu.
|
|
|
138
160
|
- Once the lane is selected, use the exact route key internally and do not invent a
|
|
139
161
|
friendlier path.
|
|
140
162
|
|
|
163
|
+
## Dedicated surface verification loop
|
|
164
|
+
|
|
165
|
+
Use this after a Movement, Life Force, or Workbench mutation or result-producing run.
|
|
166
|
+
The dedicated route family is not finished just because a write returned `ok`.
|
|
167
|
+
|
|
168
|
+
- After Movement overlays, place edits, settings changes, stay/trip repairs, or
|
|
169
|
+
deletion/invalidation work, read back the timeline, place list, settings, box
|
|
170
|
+
detail, or selection view that proves the user's practical question was answered.
|
|
171
|
+
- After Life Force profile edits, weekday-template edits, or fatigue signals, read
|
|
172
|
+
the overview back when the user is making a planning decision or wants to understand
|
|
173
|
+
the practical impact of the change.
|
|
174
|
+
- After Workbench flow creation/edit/deletion, saved-flow execution, one-off
|
|
175
|
+
execution, chat follow-up, or publish-related work, read back the flow detail, run
|
|
176
|
+
detail, node result, latest node output, published output, or run history that
|
|
177
|
+
matches the user's real goal.
|
|
178
|
+
- Do not perform a read-back as ceremony when the user only asked for a narrow save
|
|
179
|
+
and the write response already gives enough confirmation. Use it when it changes
|
|
180
|
+
understanding, verifies a repair, or grounds the next decision.
|
|
181
|
+
- In user-facing language, say what you checked: the corrected span, the weekday
|
|
182
|
+
energy picture, the flow result, the node output, or the published artifact. Keep
|
|
183
|
+
route keys and HTTP paths internal unless the user asks for implementation detail.
|
|
184
|
+
|
|
141
185
|
## Mixed-intent sequencing
|
|
142
186
|
|
|
143
187
|
Use this when one user message combines several Forge jobs, such as "review this and
|
|
@@ -187,6 +231,30 @@ worked.
|
|
|
187
231
|
write at all. Do not widen into a new taxonomy choice unless the read made the
|
|
188
232
|
container genuinely ambiguous.
|
|
189
233
|
|
|
234
|
+
## Write/read/run confirmation loop
|
|
235
|
+
|
|
236
|
+
Use this after create, update, delete, restore, run, read, or repair actions. The
|
|
237
|
+
agent should close the loop in the user's language instead of reopening intake.
|
|
238
|
+
|
|
239
|
+
- Confirm the user-facing record, action, and result, not the internal route. Mention
|
|
240
|
+
the route family only if the user asked for implementation detail or the agent is
|
|
241
|
+
reporting an API-contract problem.
|
|
242
|
+
- For batch creates and updates, confirm the working title or accepted wording, the
|
|
243
|
+
container, and the owner or placement only when those changed later retrieval,
|
|
244
|
+
accountability, or execution.
|
|
245
|
+
- If optional tags, priority, status, color, links, dates, or assignees were left
|
|
246
|
+
provisional, say that plainly once instead of asking for all of them.
|
|
247
|
+
- For action workflows, confirm the real product action: task run started or
|
|
248
|
+
completed, work adjustment applied, preference judgment or signal submitted,
|
|
249
|
+
questionnaire run updated or completed, calendar connection synced, or
|
|
250
|
+
self-observation note written.
|
|
251
|
+
- For specialized Movement, Life Force, and Workbench actions, pair the confirmation
|
|
252
|
+
with the dedicated verification loop only when the read-back changes understanding,
|
|
253
|
+
proves a repair, or grounds the next decision.
|
|
254
|
+
- Ask a follow-up only if it changes the next action: a correction, link, schedule,
|
|
255
|
+
run, publish, enrichment, preservation choice, or UI handoff. If the action is
|
|
256
|
+
complete and no decision-relevant next step is visible, stop cleanly.
|
|
257
|
+
|
|
190
258
|
## Review-before-write checkpoint
|
|
191
259
|
|
|
192
260
|
Use this when the user asks to review, guide, inspect, compare, or understand before
|
|
@@ -269,6 +337,35 @@ but not necessarily a full Psyche formulation: `questionnaire_instrument`,
|
|
|
269
337
|
uses questionnaire run actions; `self_observation` is note-backed; `wiki_page` uses
|
|
270
338
|
the wiki routes.
|
|
271
339
|
|
|
340
|
+
## Progressive disclosure after partial answers
|
|
341
|
+
|
|
342
|
+
Use this when the user has already given part of the answer. The next question should
|
|
343
|
+
show that you heard what is already settled.
|
|
344
|
+
|
|
345
|
+
- First identify what is already usable: operation, entity or surface, target record,
|
|
346
|
+
time span, working wording, owner or placement, route lane, and consent.
|
|
347
|
+
- Say the usable part back briefly, then ask only for the first missing detail that
|
|
348
|
+
would change the action: duplicate disambiguation, hierarchy parent, time
|
|
349
|
+
window, weekday, flow, run, node, correction, link, or save consent.
|
|
350
|
+
- For normal batch entities, if the accepted title or distinctive wording and the
|
|
351
|
+
meaningful body are present, do not ask for tags, priority, status, color, links,
|
|
352
|
+
dates, or assignees unless that metadata changes accountability, retrieval, or
|
|
353
|
+
execution.
|
|
354
|
+
- For specialized Movement, Life Force, and Workbench work, if the user's wording
|
|
355
|
+
already implies the lane, skip the route-family question and ask only for the
|
|
356
|
+
target span, place, weekday, profile field, flow, run, node, output, correction, or
|
|
357
|
+
consent that is still missing.
|
|
358
|
+
- For review-first work, once the practical question and scope are clear, read before
|
|
359
|
+
asking about the possible write. Do not ask the user to design a report shape unless
|
|
360
|
+
the answer would change the read.
|
|
361
|
+
- For direct Psyche saves or updates, if the user has already given a usable belief
|
|
362
|
+
sentence, functional loop, part voice, trigger episode, value phrase, event kind,
|
|
363
|
+
emotion signature, or flashcard message, ask one accuracy or consent question
|
|
364
|
+
instead of reopening origin, evidence, or repair.
|
|
365
|
+
- If the remaining unknown is only decorative optional metadata, state the provisional
|
|
366
|
+
choice and act with consent. The flow should feel like progressive clarification,
|
|
367
|
+
not a restarted form.
|
|
368
|
+
|
|
272
369
|
## Conversation arc
|
|
273
370
|
|
|
274
371
|
Most good Forge intake flows follow this sequence:
|
|
@@ -1934,6 +2031,9 @@ Direct action rules:
|
|
|
1934
2031
|
- For known-place creation or cleanup, ask what the place should be called, what
|
|
1935
2032
|
counts inside its boundary, and how future movement reads should use it. Use the
|
|
1936
2033
|
dedicated place routes, not a tag or batch entity write.
|
|
2034
|
+
- After a Movement repair, known-place edit, settings change, overlay deletion, or
|
|
2035
|
+
automatic-box invalidation, verify through the relevant dedicated read when the
|
|
2036
|
+
user is trying to understand whether the movement picture is now truthful.
|
|
1937
2037
|
|
|
1938
2038
|
Helpful follow-up lanes:
|
|
1939
2039
|
|
|
@@ -2064,6 +2164,9 @@ Direct action rules:
|
|
|
2064
2164
|
then read the overview back if they want to see the updated picture.
|
|
2065
2165
|
- After a profile or weekday-template change, read the overview back when the user is
|
|
2066
2166
|
trying to understand the practical impact of the change, not just store it silently.
|
|
2167
|
+
- After a fatigue signal, profile patch, or weekday-template edit, verify through the
|
|
2168
|
+
Life Force overview when the next planning decision depends on the updated energy
|
|
2169
|
+
picture.
|
|
2067
2170
|
|
|
2068
2171
|
Ready to act when:
|
|
2069
2172
|
|
|
@@ -2176,6 +2279,10 @@ Direct action rules:
|
|
|
2176
2279
|
- For flow chat follow-ups, use the saved flow chat route only when the user wants to
|
|
2177
2280
|
continue a flow-specific conversation. Do not turn a chat follow-up into a new flow
|
|
2178
2281
|
run, note, or generic entity update unless that is what the user asks for.
|
|
2282
|
+
- After Workbench execution, flow edits, chat follow-ups, or publish-related work,
|
|
2283
|
+
verify through the matching dedicated read: run detail, node result, latest node
|
|
2284
|
+
output, flow detail, run history, or published output. Do not leave a run or edit
|
|
2285
|
+
as an abstract success message when the user asked to inspect or use the result.
|
|
2179
2286
|
|
|
2180
2287
|
Ready to act when:
|
|
2181
2288
|
|
|
@@ -256,6 +256,28 @@ example makes the hypothesis plausible, then let the user correct the danger,
|
|
|
256
256
|
protection, payoff, cost, or wording. If the correction is usable, revise once and move
|
|
257
257
|
toward the record shape instead of asking for another broad story.
|
|
258
258
|
|
|
259
|
+
## Hypothesis Without Cross-Examination
|
|
260
|
+
|
|
261
|
+
A hypothesis is meant to reduce the user's burden of formulation, not make them prove
|
|
262
|
+
the experience. It should help the user feel, "yes, that is closer to what is
|
|
263
|
+
happening," or "no, the danger is actually over here."
|
|
264
|
+
|
|
265
|
+
- Do not turn a tentative formulation into a demand for evidence. Avoid following a
|
|
266
|
+
hypothesis with a stack of questions about evidence, origin, and repair.
|
|
267
|
+
- After a hypothesis, ask one fit-or-correction question such as, "Does that fit, or
|
|
268
|
+
is the danger/need somewhere else?"
|
|
269
|
+
- If accuracy still needs grounding, ask for the smallest lived cue or contrast that
|
|
270
|
+
would change the formulation, not a courtroom-style proof.
|
|
271
|
+
- For `belief_entry`, test the sentence, prediction, or danger it names.
|
|
272
|
+
- For `behavior_pattern`, test the cue, protection, short-term payoff, long-term
|
|
273
|
+
cost, or replacement need.
|
|
274
|
+
- For `mode_profile` and `mode_guide_session`, test the part's job, fear, burden, or
|
|
275
|
+
impulse.
|
|
276
|
+
- For `trigger_report`, test the sequence, meaning, felt stake, or consequence.
|
|
277
|
+
- For `psyche_value`, `event_type`, and `emotion_definition`, test whether the wording
|
|
278
|
+
would help future recognition rather than asking the user to justify why the record
|
|
279
|
+
matters.
|
|
280
|
+
|
|
259
281
|
## Hypothesis To Record Bridge
|
|
260
282
|
|
|
261
283
|
Once a hypothesis lands or is corrected, turn it into a saveable Forge shape instead
|
|
@@ -276,6 +298,28 @@ of leaving it as warm reflective prose.
|
|
|
276
298
|
- Save through shared batch entity routes only after the user accepts the working
|
|
277
299
|
wording or explicitly asks to save.
|
|
278
300
|
|
|
301
|
+
## Psyche progressive disclosure
|
|
302
|
+
|
|
303
|
+
Use this when the user has already supplied meaningful Psyche material. The next move
|
|
304
|
+
should preserve momentum instead of making them retell the whole story.
|
|
305
|
+
|
|
306
|
+
- Treat an offered belief sentence, value phrase, part voice, urge sentence, trigger
|
|
307
|
+
episode, event kind, emotion signature, or functional loop as real data, not as a
|
|
308
|
+
prompt to restart intake.
|
|
309
|
+
- Say what is already usable in plain language, then ask only for the missing detail
|
|
310
|
+
that changes the record: accuracy, the cue or situation, payoff or cost, protective
|
|
311
|
+
job, linked episode, whether it is a new version or an update, or save consent.
|
|
312
|
+
- If the user's wording is serviceable, keep it and refine at most one phrase. Do not
|
|
313
|
+
replace it with a prettier formulation before the user feels recognized.
|
|
314
|
+
- If the user asks to update a Psyche record and already gives the new wording, ask
|
|
315
|
+
what part of the old formulation it replaces or whether it should stand as a new
|
|
316
|
+
version. Do not reopen origin, evidence, or repair unless the new meaning is still
|
|
317
|
+
unclear.
|
|
318
|
+
- If the working material is already accurate enough, ask one accuracy or consent
|
|
319
|
+
question instead of reopening origin, evidence, or repair.
|
|
320
|
+
- If the remaining unknown is optional therapeutic metadata, save a provisional
|
|
321
|
+
version after one accuracy check and preserve nuance in a linked note when needed.
|
|
322
|
+
|
|
279
323
|
## Psyche save-readiness checkpoint
|
|
280
324
|
|
|
281
325
|
Use this before asking another deepening question. Psyche work should feel careful,
|
|
@@ -307,6 +351,23 @@ save the record instead of reopening the whole story.
|
|
|
307
351
|
- After the minimum is present, ask one accuracy question at most: "Is this true
|
|
308
352
|
enough to save as a first version?" If yes, save through shared batch CRUD.
|
|
309
353
|
|
|
354
|
+
## Psyche after-save close
|
|
355
|
+
|
|
356
|
+
Use this after a Psyche record is created or updated. The user should feel the
|
|
357
|
+
formulation was held accurately, not that a new worksheet has started.
|
|
358
|
+
|
|
359
|
+
- Confirm the accepted wording, primary container, and whether it was saved as a
|
|
360
|
+
first version, update, link, archive, or distinct version.
|
|
361
|
+
- Do not reopen origin, evidence, repair, or adjacent entity mapping after the save.
|
|
362
|
+
Only offer a flashcard, note, value link, task, or habit when that next object is
|
|
363
|
+
already visible and would materially help retrieval or action.
|
|
364
|
+
- If nuance was preserved in a linked note or left provisional, say that briefly and
|
|
365
|
+
keep the door open for later correction without asking another broad exploration
|
|
366
|
+
question.
|
|
367
|
+
- For belief, pattern, mode, trigger report, value, event type, emotion definition,
|
|
368
|
+
or flashcard saves, the clean close is one accurate sentence plus any concrete next option
|
|
369
|
+
that genuinely follows from the user's request.
|
|
370
|
+
|
|
310
371
|
## Psyche Hypothesis Map
|
|
311
372
|
|
|
312
373
|
Use these shapes after at least one concrete example is clear. The hypothesis should
|