1use crate::event_protocol::{EventEnvelopeV1, EventProtocolError};
9use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
10use std::fmt;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{SystemTime, UNIX_EPOCH};
13use thiserror::Error;
14
15pub const CORE_IDENTITY_SCHEMA_V1: &str = "a3s.code.core-identity.v1";
16pub const CORE_EVENT_IDENTITY_SCHEMA_V1: &str = "a3s.code.core-event-identity.v1";
17pub const CORE_EVENT_PAYLOAD_DIGEST_DOMAIN_V1: &str = "a3s.code.core-event.payload.v1";
18pub const CORE_EVENT_IDENTITY_DIGEST_DOMAIN_V1: &str = "a3s.code.core-event.identity.v1";
19pub const CORE_IDENTITY_MAX_ID_BYTES: usize = 256;
20pub const CORE_IDENTITY_MAX_EVENT_TYPE_BYTES: usize = 128;
21pub const CORE_IDENTITY_MAX_PAYLOAD_BYTES: usize = 4 * 1024 * 1024;
22pub const CORE_IDENTITY_MAX_MEDIA_TYPE_BYTES: usize = 256;
23pub const CORE_IDENTITY_MAX_ARTIFACT_BYTES: u64 = 4 * 1024 * 1024 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Error)]
26pub enum CoreIdentityError {
27 #[error("unsupported core identity schema")]
28 UnsupportedSchema,
29 #[error("core identity field `{0}` is invalid")]
30 InvalidField(&'static str),
31 #[error("core identity digest `{0}` is invalid")]
32 InvalidDigest(&'static str),
33 #[error("core identity sequence overflow")]
34 SequenceOverflow,
35 #[error("core identity serialization failed: {0}")]
36 Serialization(String),
37 #[error("agent event cannot be adapted to a core identity: {0}")]
38 EventProtocol(String),
39}
40
41#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
43#[serde(transparent)]
44pub struct OperationId(Box<str>);
45
46impl OperationId {
47 pub fn new(value: impl Into<String>) -> Result<Self, CoreIdentityError> {
48 let value = value.into();
49 validate_id("operation_id", &value)?;
50 Ok(Self(value.into_boxed_str()))
51 }
52
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl<'de> Deserialize<'de> for OperationId {
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: Deserializer<'de>,
62 {
63 let value = String::deserialize(deserializer)?;
64 Self::new(value).map_err(D::Error::custom)
65 }
66}
67
68impl fmt::Display for OperationId {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74#[derive(
78 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
79)]
80#[serde(transparent)]
81pub struct SourceRevision(u64);
82
83impl SourceRevision {
84 pub const fn new(value: u64) -> Self {
85 Self(value)
86 }
87
88 pub const fn unknown() -> Self {
89 Self(0)
90 }
91
92 pub const fn value(self) -> u64 {
93 self.0
94 }
95
96 pub const fn is_known(self) -> bool {
97 self.0 != 0
98 }
99
100 pub fn next(self) -> Result<Self, CoreIdentityError> {
101 self.0
102 .checked_add(1)
103 .map(Self)
104 .ok_or(CoreIdentityError::SequenceOverflow)
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
110#[serde(rename_all = "camelCase", deny_unknown_fields)]
111pub struct CapabilityStamp {
112 generation: u64,
113 digest: String,
114}
115
116impl CapabilityStamp {
117 pub fn new(generation: u64, digest: impl Into<String>) -> Result<Self, CoreIdentityError> {
118 if generation == 0 {
119 return Err(CoreIdentityError::InvalidField("generation"));
120 }
121 let digest = digest.into();
122 validate_digest("digest", &digest)?;
123 Ok(Self { generation, digest })
124 }
125
126 pub const fn generation(&self) -> u64 {
127 self.generation
128 }
129
130 pub fn digest(&self) -> &str {
131 &self.digest
132 }
133}
134
135impl<'de> Deserialize<'de> for CapabilityStamp {
136 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137 where
138 D: Deserializer<'de>,
139 {
140 #[derive(Deserialize)]
141 #[serde(rename_all = "camelCase", deny_unknown_fields)]
142 struct Wire {
143 generation: u64,
144 digest: String,
145 }
146
147 let wire = Wire::deserialize(deserializer)?;
148 Self::new(wire.generation, wire.digest).map_err(D::Error::custom)
149 }
150}
151
152#[derive(
154 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
155)]
156#[serde(transparent)]
157pub struct EvidenceCursor(u64);
158
159impl EvidenceCursor {
160 pub const fn new(sequence: u64) -> Self {
161 Self(sequence)
162 }
163
164 pub const fn sequence(self) -> u64 {
165 self.0
166 }
167
168 pub fn next(self) -> Result<Self, CoreIdentityError> {
169 self.0
170 .checked_add(1)
171 .map(Self)
172 .ok_or(CoreIdentityError::SequenceOverflow)
173 }
174}
175
176#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
179#[serde(rename_all = "camelCase", deny_unknown_fields)]
180pub struct ArtifactRef {
181 digest: String,
182 media_type: String,
183 size_bytes: u64,
184}
185
186impl ArtifactRef {
187 pub fn new(
188 digest: impl Into<String>,
189 media_type: impl Into<String>,
190 size_bytes: u64,
191 ) -> Result<Self, CoreIdentityError> {
192 let digest = digest.into();
193 validate_digest("digest", &digest)?;
194 let media_type = media_type.into();
195 if media_type.is_empty()
196 || media_type.len() > CORE_IDENTITY_MAX_MEDIA_TYPE_BYTES
197 || media_type.contains('\0')
198 || media_type.lines().count() != 1
199 {
200 return Err(CoreIdentityError::InvalidField("media_type"));
201 }
202 if size_bytes > CORE_IDENTITY_MAX_ARTIFACT_BYTES {
203 return Err(CoreIdentityError::InvalidField("size_bytes"));
204 }
205 Ok(Self {
206 digest,
207 media_type,
208 size_bytes,
209 })
210 }
211
212 pub fn digest(&self) -> &str {
213 &self.digest
214 }
215
216 pub fn media_type(&self) -> &str {
217 &self.media_type
218 }
219
220 pub const fn size_bytes(&self) -> u64 {
221 self.size_bytes
222 }
223
224 pub fn validate(&self) -> Result<(), CoreIdentityError> {
225 Self::new(
226 self.digest.clone(),
227 self.media_type.clone(),
228 self.size_bytes,
229 )
230 .map(|_| ())
231 }
232}
233
234impl<'de> Deserialize<'de> for ArtifactRef {
235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236 where
237 D: Deserializer<'de>,
238 {
239 #[derive(Deserialize)]
240 #[serde(rename_all = "camelCase", deny_unknown_fields)]
241 struct Wire {
242 digest: String,
243 media_type: String,
244 size_bytes: u64,
245 }
246
247 let wire = Wire::deserialize(deserializer)?;
248 Self::new(wire.digest, wire.media_type, wire.size_bytes).map_err(D::Error::custom)
249 }
250}
251
252#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
254#[serde(rename_all = "camelCase", deny_unknown_fields)]
255pub struct CoreIdentity {
256 pub schema: String,
257 pub operation_id: OperationId,
258 pub source_revision: SourceRevision,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub capability_stamp: Option<CapabilityStamp>,
261 pub evidence_cursor: EvidenceCursor,
262}
263
264impl CoreIdentity {
265 pub fn new(
266 operation_id: OperationId,
267 source_revision: SourceRevision,
268 capability_stamp: Option<CapabilityStamp>,
269 evidence_cursor: EvidenceCursor,
270 ) -> Self {
271 Self {
272 schema: CORE_IDENTITY_SCHEMA_V1.to_owned(),
273 operation_id,
274 source_revision,
275 capability_stamp,
276 evidence_cursor,
277 }
278 }
279
280 pub fn validate(&self) -> Result<(), CoreIdentityError> {
281 if self.schema != CORE_IDENTITY_SCHEMA_V1 {
282 return Err(CoreIdentityError::UnsupportedSchema);
283 }
284 Ok(())
285 }
286}
287
288impl<'de> Deserialize<'de> for CoreIdentity {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: Deserializer<'de>,
292 {
293 #[derive(Deserialize)]
294 #[serde(rename_all = "camelCase", deny_unknown_fields)]
295 struct Wire {
296 schema: String,
297 operation_id: OperationId,
298 source_revision: SourceRevision,
299 #[serde(default)]
300 capability_stamp: Option<CapabilityStamp>,
301 evidence_cursor: EvidenceCursor,
302 }
303
304 let wire = Wire::deserialize(deserializer)?;
305 let value = Self {
306 schema: wire.schema,
307 operation_id: wire.operation_id,
308 source_revision: wire.source_revision,
309 capability_stamp: wire.capability_stamp,
310 evidence_cursor: wire.evidence_cursor,
311 };
312 value.validate().map_err(D::Error::custom)?;
313 Ok(value)
314 }
315}
316
317#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
319#[serde(rename_all = "camelCase", deny_unknown_fields)]
320pub struct CoreEventIdentity {
321 pub schema: String,
322 pub identity: CoreIdentity,
323 pub event_type: String,
324 pub payload_digest: String,
325 pub payload_bytes: u64,
326 pub observed_at_ms: u64,
327 pub event_digest: String,
328}
329
330pub(crate) struct CanonicalEventPayload {
334 pub(crate) event_type: String,
335 pub(crate) payload: Vec<u8>,
336 pub(crate) wire: Vec<u8>,
337}
338
339pub(crate) fn canonical_event_payload(
340 event: &crate::AgentEvent,
341) -> Result<CanonicalEventPayload, CoreIdentityError> {
342 let envelope = EventEnvelopeV1::try_from(event).map_err(map_event_protocol_error)?;
343 let payload = serde_json::to_vec(&envelope.payload)
344 .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
345 let wire = serde_json::to_vec(event)
346 .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
347 Ok(CanonicalEventPayload {
348 event_type: envelope.event_type,
349 payload,
350 wire,
351 })
352}
353
354impl CoreEventIdentity {
355 pub fn from_agent_event(
356 identity: CoreIdentity,
357 observed_at_ms: u64,
358 event: &crate::AgentEvent,
359 ) -> Result<Self, CoreIdentityError> {
360 identity.validate()?;
361 let canonical = canonical_event_payload(event)?;
362 let payload = canonical.payload;
363 if payload.is_empty() || payload.len() > CORE_IDENTITY_MAX_PAYLOAD_BYTES {
364 return Err(CoreIdentityError::InvalidField("payload_bytes"));
365 }
366 let payload_bytes = u64::try_from(payload.len())
367 .map_err(|_| CoreIdentityError::InvalidField("payload_bytes"))?;
368 let mut value = Self {
369 schema: CORE_EVENT_IDENTITY_SCHEMA_V1.to_owned(),
370 identity,
371 event_type: canonical.event_type,
372 payload_digest: digest_bytes(CORE_EVENT_PAYLOAD_DIGEST_DOMAIN_V1, &payload),
373 payload_bytes,
374 observed_at_ms,
375 event_digest: String::new(),
376 };
377 value.validate_without_digest()?;
378 value.event_digest = value.expected_digest()?;
379 Ok(value)
380 }
381
382 pub fn from_agent_event_at(
386 identity: CoreIdentity,
387 clock: &dyn LogicalClock,
388 event: &crate::AgentEvent,
389 ) -> Result<Self, CoreIdentityError> {
390 Self::from_agent_event(identity, clock.now_ms(), event)
391 }
392
393 pub fn from_run_event(
395 operation_id: OperationId,
396 source_revision: SourceRevision,
397 capability_stamp: Option<CapabilityStamp>,
398 record: &crate::run::RunEventRecord,
399 ) -> Result<Self, CoreIdentityError> {
400 let sequence = u64::try_from(record.sequence)
401 .map_err(|_| CoreIdentityError::InvalidField("sequence"))?;
402 Self::from_agent_event(
403 CoreIdentity::new(
404 operation_id,
405 source_revision,
406 capability_stamp,
407 EvidenceCursor::new(sequence),
408 ),
409 record.timestamp_ms,
410 &record.event,
411 )
412 }
413
414 pub fn validate(&self) -> Result<(), CoreIdentityError> {
415 self.validate_without_digest()?;
416 validate_digest("event_digest", &self.event_digest)?;
417 if self.event_digest != self.expected_digest()? {
418 return Err(CoreIdentityError::InvalidField("event_digest"));
419 }
420 Ok(())
421 }
422
423 pub fn expected_digest(&self) -> Result<String, CoreIdentityError> {
424 #[derive(Serialize)]
425 struct Identity<'a> {
426 schema: &'a str,
427 identity: &'a CoreIdentity,
428 event_type: &'a str,
429 payload_digest: &'a str,
430 payload_bytes: u64,
431 observed_at_ms: u64,
432 }
433 digest_json(
434 CORE_EVENT_IDENTITY_DIGEST_DOMAIN_V1,
435 &Identity {
436 schema: &self.schema,
437 identity: &self.identity,
438 event_type: &self.event_type,
439 payload_digest: &self.payload_digest,
440 payload_bytes: self.payload_bytes,
441 observed_at_ms: self.observed_at_ms,
442 },
443 )
444 }
445
446 fn validate_without_digest(&self) -> Result<(), CoreIdentityError> {
447 if self.schema != CORE_EVENT_IDENTITY_SCHEMA_V1 {
448 return Err(CoreIdentityError::UnsupportedSchema);
449 }
450 self.identity.validate()?;
451 if self.event_type.is_empty()
452 || self.event_type.len() > CORE_IDENTITY_MAX_EVENT_TYPE_BYTES
453 || self.event_type.starts_with('.')
454 || self.event_type.ends_with('.')
455 || !self.event_type.bytes().all(|byte| {
456 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
457 })
458 {
459 return Err(CoreIdentityError::InvalidField("event_type"));
460 }
461 validate_digest("payload_digest", &self.payload_digest)?;
462 if self.payload_bytes == 0 || self.payload_bytes > CORE_IDENTITY_MAX_PAYLOAD_BYTES as u64 {
463 return Err(CoreIdentityError::InvalidField("payload_bytes"));
464 }
465 if self.observed_at_ms == 0 {
466 return Err(CoreIdentityError::InvalidField("observed_at_ms"));
467 }
468 Ok(())
469 }
470}
471
472impl<'de> Deserialize<'de> for CoreEventIdentity {
473 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
474 where
475 D: Deserializer<'de>,
476 {
477 #[derive(Deserialize)]
478 #[serde(rename_all = "camelCase", deny_unknown_fields)]
479 struct Wire {
480 schema: String,
481 identity: CoreIdentity,
482 event_type: String,
483 payload_digest: String,
484 payload_bytes: u64,
485 observed_at_ms: u64,
486 event_digest: String,
487 }
488
489 let wire = Wire::deserialize(deserializer)?;
490 let value = Self {
491 schema: wire.schema,
492 identity: wire.identity,
493 event_type: wire.event_type,
494 payload_digest: wire.payload_digest,
495 payload_bytes: wire.payload_bytes,
496 observed_at_ms: wire.observed_at_ms,
497 event_digest: wire.event_digest,
498 };
499 value.validate().map_err(D::Error::custom)?;
500 Ok(value)
501 }
502}
503
504pub trait LogicalClock: Send + Sync {
506 fn now_ms(&self) -> u64;
507}
508
509#[derive(Clone, Copy, Debug, Default)]
510pub struct SystemLogicalClock;
511
512impl LogicalClock for SystemLogicalClock {
513 fn now_ms(&self) -> u64 {
514 SystemTime::now()
515 .duration_since(UNIX_EPOCH)
516 .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
517 .unwrap_or(0)
518 }
519}
520
521#[derive(Debug)]
522pub struct ManualLogicalClock {
523 now_ms: AtomicU64,
524}
525
526impl ManualLogicalClock {
527 pub const fn new(initial_ms: u64) -> Self {
528 Self {
529 now_ms: AtomicU64::new(initial_ms),
530 }
531 }
532
533 pub fn set(&self, value: u64) {
534 self.now_ms.store(value, Ordering::SeqCst);
535 }
536
537 pub fn advance(&self, delta_ms: u64) -> Result<u64, CoreIdentityError> {
538 let mut current = self.now_ms.load(Ordering::SeqCst);
539 loop {
540 let next = current
541 .checked_add(delta_ms)
542 .ok_or(CoreIdentityError::SequenceOverflow)?;
543 match self
544 .now_ms
545 .compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst)
546 {
547 Ok(_) => return Ok(next),
548 Err(actual) => current = actual,
549 }
550 }
551 }
552}
553
554impl LogicalClock for ManualLogicalClock {
555 fn now_ms(&self) -> u64 {
556 self.now_ms.load(Ordering::SeqCst)
557 }
558}
559
560fn map_event_protocol_error(error: EventProtocolError) -> CoreIdentityError {
561 CoreIdentityError::EventProtocol(error.to_string())
562}
563
564fn validate_id(field: &'static str, value: &str) -> Result<(), CoreIdentityError> {
565 if value.is_empty()
566 || value.len() > CORE_IDENTITY_MAX_ID_BYTES
567 || value.contains('\0')
568 || value.lines().count() != 1
569 {
570 return Err(CoreIdentityError::InvalidField(field));
571 }
572 Ok(())
573}
574
575fn validate_digest(field: &'static str, value: &str) -> Result<(), CoreIdentityError> {
576 if value.len() != 71
577 || !value.starts_with("sha256:")
578 || !value[7..]
579 .bytes()
580 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
581 {
582 return Err(CoreIdentityError::InvalidDigest(field));
583 }
584 Ok(())
585}
586
587fn digest_bytes(domain: &str, bytes: &[u8]) -> String {
588 use sha2::{Digest, Sha256};
589 let mut hasher = Sha256::new();
590 hasher.update(domain.as_bytes());
591 hasher.update([0]);
592 hasher.update(bytes);
593 format!("sha256:{:x}", hasher.finalize())
594}
595
596fn digest_json<T: Serialize>(domain: &str, value: &T) -> Result<String, CoreIdentityError> {
597 let bytes = serde_json::to_vec(value)
598 .map_err(|error| CoreIdentityError::Serialization(error.to_string()))?;
599 Ok(digest_bytes(domain, &bytes))
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::AgentEvent;
606 use serde_json::Value;
607
608 fn digest(ch: char) -> String {
609 format!("sha256:{}", ch.to_string().repeat(64))
610 }
611
612 fn identity(cursor: u64) -> CoreIdentity {
613 CoreIdentity::new(
614 OperationId::new("session-1/run-1").unwrap(),
615 SourceRevision::new(7),
616 Some(CapabilityStamp::new(3, digest('a')).unwrap()),
617 EvidenceCursor::new(cursor),
618 )
619 }
620
621 #[test]
622 fn event_adapter_binds_typed_identity_and_is_replay_stable() {
623 let first = CoreEventIdentity::from_agent_event(
624 identity(4),
625 42,
626 &AgentEvent::TextDelta {
627 text: "evidence".to_owned(),
628 },
629 )
630 .unwrap();
631 let second = CoreEventIdentity::from_agent_event(
632 identity(4),
633 42,
634 &AgentEvent::TextDelta {
635 text: "evidence".to_owned(),
636 },
637 )
638 .unwrap();
639
640 assert_eq!(first, second);
641 assert_eq!(first.identity.evidence_cursor.sequence(), 4);
642 assert_eq!(first.event_type, "text_delta");
643 assert!(first.validate().is_ok());
644 assert!(
645 serde_json::from_value::<CoreEventIdentity>(serde_json::to_value(&first).unwrap())
646 .is_ok()
647 );
648 }
649
650 #[test]
651 fn retained_run_event_uses_its_cursor_and_observation_time() {
652 let record = crate::run::RunEventRecord {
653 sequence: 9,
654 timestamp_ms: 77,
655 event: AgentEvent::TextDelta {
656 text: "done".to_owned(),
657 },
658 };
659 let projected = record
660 .core_identity(
661 OperationId::new("session-1/run-1").unwrap(),
662 SourceRevision::new(8),
663 None,
664 )
665 .unwrap();
666 assert_eq!(projected.identity.evidence_cursor.sequence(), 9);
667 assert_eq!(projected.observed_at_ms, 77);
668 assert_eq!(projected.event_type, "text_delta");
669 }
670
671 #[test]
672 fn deserialization_rejects_tampered_digest_and_unknown_fields() {
673 let event = CoreEventIdentity::from_agent_event(
674 identity(0),
675 42,
676 &AgentEvent::Start {
677 prompt: "run".to_owned(),
678 },
679 )
680 .unwrap();
681 let mut value = serde_json::to_value(&event).unwrap();
682 value["eventDigest"] = Value::String(digest('b'));
683 assert!(serde_json::from_value::<CoreEventIdentity>(value).is_err());
684
685 let mut value = serde_json::to_value(&event).unwrap();
686 value["unexpected"] = Value::Bool(true);
687 assert!(serde_json::from_value::<CoreEventIdentity>(value).is_err());
688
689 let artifact = ArtifactRef::new(digest('a'), "text/plain", 3).unwrap();
690 let mut value = serde_json::to_value(&artifact).unwrap();
691 value["sizeBytes"] = Value::from(CORE_IDENTITY_MAX_ARTIFACT_BYTES + 1);
692 assert!(serde_json::from_value::<ArtifactRef>(value).is_err());
693 }
694
695 #[test]
696 fn manual_clock_is_injectable_and_overflow_is_explicit() {
697 let clock = ManualLogicalClock::new(10);
698 assert_eq!(clock.now_ms(), 10);
699 assert_eq!(clock.advance(5).unwrap(), 15);
700 clock.set(u64::MAX);
701 assert_eq!(clock.advance(1), Err(CoreIdentityError::SequenceOverflow));
702 }
703
704 #[test]
705 fn event_adapter_can_use_an_injected_clock() {
706 let clock = ManualLogicalClock::new(123);
707 let event = CoreEventIdentity::from_agent_event_at(
708 identity(0),
709 &clock,
710 &AgentEvent::Start {
711 prompt: "run".to_owned(),
712 },
713 )
714 .unwrap();
715 assert_eq!(event.observed_at_ms, 123);
716 clock.advance(7).unwrap();
717 let next = CoreEventIdentity::from_agent_event_at(
718 identity(1),
719 &clock,
720 &AgentEvent::TextDelta {
721 text: "step".to_owned(),
722 },
723 )
724 .unwrap();
725 assert_eq!(next.observed_at_ms, 130);
726 }
727
728 #[test]
729 fn typed_values_reject_invalid_wire_data() {
730 assert!(OperationId::new("bad\noperation").is_err());
731 assert!(CapabilityStamp::new(0, digest('a')).is_err());
732 assert!(ArtifactRef::new(digest('a'), "", 0).is_err());
733 assert_eq!(
734 EvidenceCursor::new(u64::MAX).next(),
735 Err(CoreIdentityError::SequenceOverflow)
736 );
737 }
738}