@atmem/architect 0.2.0-alpha.4.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +19 -21
  4. package/dist/assets/index-BTLHaa3J.css +1 -0
  5. package/dist/assets/{index-qtCM2D7O.js → index-CQ2C1gKF.js} +19206 -19139
  6. package/dist/assets/{index.es-C6C0sneB.js → index.es-DTyqIdaO.js} +1 -1
  7. package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-Ox0j5xj5.js} +3 -3
  8. package/dist/index.html +2 -2
  9. package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
  10. package/package.json +2 -2
  11. package/public/licenses/atmem-APACHE-2.0.txt +202 -0
  12. package/server/app.py +1 -1
  13. package/server/codex_app_server.py +5 -5
  14. package/server/codex_worker_policy.py +27 -4
  15. package/server/engineering_artifacts.py +7 -1
  16. package/server/engineering_handoff.py +9 -0
  17. package/server/engineering_routes.py +16 -2
  18. package/server/local_providers.py +7 -3
  19. package/server/memory_context.py +58 -0
  20. package/server/memory_routes.py +67 -8
  21. package/server/memory_service.py +37 -4
  22. package/server/memory_worker.py +72 -9
  23. package/server/orca_handoff.py +90 -0
  24. package/server/pricing.py +106 -0
  25. package/server/prompts.py +13 -0
  26. package/server/windows_snapshot.py +119 -0
  27. package/src/ArchitectureWorkPanel.jsx +0 -7
  28. package/src/ArtifactChecks.jsx +2 -2
  29. package/src/ChatMessage.jsx +12 -2
  30. package/src/EngagementViews.jsx +18 -16
  31. package/src/EngineeringPanel.jsx +27 -27
  32. package/src/FileTree.jsx +7 -0
  33. package/src/FinalChecklist.jsx +15 -0
  34. package/src/HelpNote.jsx +3 -0
  35. package/src/ImplementationDelivery.jsx +52 -16
  36. package/src/JourneyViews.jsx +19 -16
  37. package/src/LocationChoices.jsx +12 -0
  38. package/src/MemoryPanel.jsx +43 -31
  39. package/src/OpenAIVoicePanel.jsx +15 -14
  40. package/src/OutputsView.jsx +21 -15
  41. package/src/ProjectChecks.jsx +44 -0
  42. package/src/WorkbenchDialogs.jsx +1 -1
  43. package/src/conversation.mjs +1 -0
  44. package/src/delivery.mjs +1 -1
  45. package/src/estimate.mjs +5 -3
  46. package/src/final-package.mjs +34 -0
  47. package/src/journey-workspace.css +34 -0
  48. package/src/journey.mjs +12 -2
  49. package/src/main.jsx +119 -96
  50. package/src/output-bundle.mjs +4 -2
  51. package/src/pricing.mjs +56 -2
  52. package/src/useChatScroll.jsx +5 -4
  53. package/src/useEngineeringStatus.mjs +17 -0
  54. package/src/validation-receipt.mjs +1 -1
  55. package/dist/assets/index-B97MkVMs.css +0 -1
@@ -4,6 +4,10 @@ from pathlib import Path
4
4
 
5
5
  SIGN_IN={'claude-code':('claude','claude auth login'),'codex':('codex','codex login'),'gemini-cli':('gemini','gemini'),'copilot':('copilot','copilot login')}
6
6
 
7
+ def cli_command(argv):
8
+ """Resolve PATHEXT shims explicitly; CreateProcess does not search .cmd names."""
9
+ return [shutil.which(argv[0]) or argv[0], *argv[1:]]
10
+
7
11
  def account_environment():
8
12
  env=dict(os.environ)
9
13
  for key in ['ANTHROPIC_API_KEY','ANTHROPIC_AUTH_TOKEN','OPENAI_API_KEY','CODEX_API_KEY','GEMINI_API_KEY','GOOGLE_API_KEY','GITHUB_TOKEN','GH_TOKEN','COPILOT_GITHUB_TOKEN']:
@@ -21,7 +25,7 @@ def signed_in(provider):
21
25
  if not argv or not shutil.which(argv[0]):return None
22
26
  import subprocess
23
27
  try:
24
- done=subprocess.run(argv,capture_output=True,text=True,timeout=8,stdin=subprocess.DEVNULL,env=account_environment())
28
+ done=subprocess.run(cli_command(argv),capture_output=True,text=True,encoding='utf-8',errors='replace',timeout=8,stdin=subprocess.DEVNULL,env=account_environment())
25
29
  except Exception:
26
30
  return None
27
31
  if provider=='claude-code':
@@ -40,7 +44,7 @@ async def start_login(provider):
40
44
  if not argv or not shutil.which(argv[0]):raise RuntimeError('This provider has no sign-in AtMem AI Architect can start. Use its API key instead.')
41
45
  running=LOGINS.get(provider)
42
46
  if running and running.returncode is None:return {'started':True,'url':None}
43
- process=await asyncio.create_subprocess_exec(*argv,stdin=asyncio.subprocess.DEVNULL,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.STDOUT,env=account_environment(),start_new_session=os.name!='nt')
47
+ process=await asyncio.create_subprocess_exec(*cli_command(argv),stdin=asyncio.subprocess.DEVNULL,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.STDOUT,env=account_environment(),start_new_session=os.name!='nt')
44
48
  LOGINS[provider]=process
45
49
  url=None
46
50
  try:
@@ -128,7 +132,7 @@ def models(provider):
128
132
  return [{'id':i,'name':n,'efforts':CLAUDE_EFFORTS,'defaultEffort':'medium'} for i,n in CLAUDE_MODELS]
129
133
  if provider=='codex' and shutil.which('codex'):
130
134
  try:
131
- done=subprocess.run(['codex','debug','models'],capture_output=True,text=True,timeout=15,stdin=subprocess.DEVNULL,env=account_environment())
135
+ done=subprocess.run(cli_command(['codex','debug','models']),capture_output=True,text=True,encoding='utf-8',errors='replace',timeout=15,stdin=subprocess.DEVNULL,env=account_environment())
132
136
  data=json.loads(done.stdout)
133
137
  return [{'id':m['slug'],'name':m.get('display_name',m['slug']),'efforts':[e['effort'] for e in m.get('supported_reasoning_levels',[])],'defaultEffort':m.get('default_reasoning_level')}
134
138
  for m in data.get('models',[]) if m.get('visibility','list')=='list']
@@ -0,0 +1,58 @@
1
+ """Operator-configured external RAG/memory retrieval; never persists returned context."""
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ import json
4
+ import os
5
+ import re
6
+ from urllib.parse import urlsplit
7
+ import urllib.request
8
+ from .settings import setting
9
+
10
+
11
+ def providers():
12
+ values=json.loads(setting('MEMORY_CONTEXT_PROVIDERS') or '[]')
13
+ if not isinstance(values,list)or len(values)>4:raise ValueError('Configure at most four context providers')
14
+ seen=set()
15
+ for value in values:
16
+ if not isinstance(value,dict)or set(value)-{'id','label','url','tokenEnv','enabled','kind'}:raise ValueError('Invalid context provider configuration')
17
+ ident=value.get('id','');url=urlsplit(value.get('url',''))
18
+ if not re.fullmatch(r'[a-z][a-z0-9-]{0,63}',ident)or ident in seen or url.scheme not in {'http','https'}or not url.hostname or url.username or url.password or url.fragment or url.query:raise ValueError('Context providers need unique IDs and credential-free HTTP endpoints')
19
+ token=value.get('tokenEnv')
20
+ if token and not re.fullmatch(r'[A-Z][A-Z0-9_]{0,99}',token):raise ValueError('Use an environment variable for provider credentials')
21
+ seen.add(ident)
22
+ return values
23
+
24
+
25
+ def describe():
26
+ return [{'id':value['id'],'label':str(value.get('label')or value['id'])[:80],'kind':value.get('kind','rag'),'enabled':value.get('enabled',True),'configured':True}for value in providers()]
27
+
28
+
29
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
30
+ def redirect_request(self,*args,**kwargs):return None
31
+
32
+
33
+ def retrieve(project,owner,query,safe):
34
+ configured=[value for value in providers()if value.get('enabled',True)]
35
+ if not configured:return [],[]
36
+ def fetch(value):
37
+ ident=value['id'];headers={'Content-Type':'application/json'}
38
+ if value.get('tokenEnv'):
39
+ token=os.getenv(value['tokenEnv'])
40
+ if not token:return [],{'id':ident,'status':'credential_missing'}
41
+ headers['Authorization']='Bearer '+token
42
+ request=urllib.request.Request(value['url'],data=json.dumps({'query':query[:4000],'userId':owner,'projectId':project,'limit':6}).encode(),headers=headers,method='POST')
43
+ try:
44
+ with urllib.request.build_opener(NoRedirect).open(request,timeout=4)as response:data=response.read(100001)
45
+ if len(data)>100000:raise ValueError('Context exceeds bound')
46
+ result=json.loads(data);rows=result.get('records')
47
+ if not isinstance(rows,list)or len(rows)>6:raise ValueError('Invalid context response')
48
+ context=[]
49
+ for row in rows:
50
+ scope=row.get('scope','project')
51
+ if scope not in {'project','user'}or row.get('userId')!=owner or(scope=='project'and row.get('projectId')!=project)or(scope=='user'and row.get('projectId')not in {None,''}):raise ValueError('Context scope mismatch')
52
+ text=row.get('text');source=row.get('source');record_id=row.get('id')
53
+ if not isinstance(text,str)or not 0<len(text)<=4000 or not safe(text)or not isinstance(source,dict)or not isinstance(record_id,str)or len(record_id)>160 or not safe(source):raise ValueError('Invalid context provenance')
54
+ context.append({'id':ident+':'+record_id,'scope':scope,'text':text,'sourceDate':row.get('createdAt'),'key':row.get('key'),'provider':ident,'source':source,'external':True,'boundary':'retrieved_context_only'})
55
+ return context,{'id':ident,'status':'ready','records':len(context)}
56
+ except Exception:return [],{'id':ident,'status':'unavailable'}
57
+ with ThreadPoolExecutor(max_workers=4)as pool:results=list(pool.map(fetch,configured))
58
+ return [row for rows,_ in results for row in rows],[state for _,state in results]
@@ -6,7 +6,7 @@ from typing import Literal
6
6
  from .contracts import Strict
7
7
  from .memory_service import service
8
8
  from . import skills
9
- from .pricing import aws_s3, storage_quote
9
+ from .pricing import aws_s3, storage_quote, lambda_quote, google_public_quote
10
10
 
11
11
  router=APIRouter()
12
12
  class Target(Strict):
@@ -28,8 +28,8 @@ class Price(Target):
28
28
  provider:Literal['aws','azure','gcp','ibm']='aws'
29
29
  region:str=Field(max_length=40)
30
30
 
31
- async def invoke(fn,*args):
32
- try:return await asyncio.to_thread(fn,*args)
31
+ async def invoke(fn,*args,**kwargs):
32
+ try:return await asyncio.to_thread(fn,*args,**kwargs)
33
33
  except ValueError as exc:raise HTTPException(422,str(exc))
34
34
  except Exception:raise HTTPException(503,'AtMem is unavailable; your project still works. Run npm run setup:memory if needed.')
35
35
 
@@ -54,11 +54,8 @@ async def profile(payload:Profile):
54
54
 
55
55
  @router.post('/api/memory/forget-preview')
56
56
  async def preview(payload:Forget):
57
- report=await invoke(service().inspect,payload.projectId)
58
- records=report['profile' if payload.scope=='user' else 'project']
59
- record=next((r for r in records if r['id']==payload.recordId),None)
60
- if not record:raise HTTPException(404,'Record not available in this scope')
61
- return {'records':[r for r in records if r['fact_key']==record['fact_key']], 'notice':'Removes this memory and its earlier values. Project data is kept; edit the project itself to change requirements or decisions.'}
57
+ result=await invoke(service().record_detail,payload.projectId,payload.scope,payload.recordId,True)
58
+ return {**result,'notice':'Removes this memory and its history. Project files remain.'}
62
59
 
63
60
  @router.post('/api/memory/forget')
64
61
  async def forget(payload:Forget):
@@ -80,3 +77,65 @@ async def price(payload:Price):
80
77
  async def storage(payload:Price):
81
78
  try:return await asyncio.to_thread(storage_quote,payload.provider,payload.region)
82
79
  except Exception:raise HTTPException(503,'The exact provider/region price could not be verified. No rate was invented.')
80
+
81
+
82
+ class LambdaPrice(Target):
83
+ region:str=Field(max_length=40)
84
+ architecture:Literal['x86_64','arm64']='x86_64'
85
+
86
+ @router.post('/api/pricing/lambda')
87
+ async def lambda_price(payload:LambdaPrice):
88
+ try:return await asyncio.to_thread(lambda_quote,payload.region,payload.architecture)
89
+ except Exception:raise HTTPException(503,'The regional Lambda price is unavailable. Retry pricing; no rate was invented.')
90
+
91
+
92
+ class GooglePrice(Target):
93
+ service:Literal['cloud-run','cloud-sql']
94
+ region:str=Field(default='',max_length=40)
95
+ planningReference:StrictBool=False
96
+
97
+ @router.post('/api/pricing/google')
98
+ async def google_price(payload:GooglePrice):
99
+ try:return await asyncio.to_thread(google_public_quote,payload.service,payload.region,payload.planningReference)
100
+ except ValueError as error:raise HTTPException(422,str(error)) from None
101
+ except Exception:raise HTTPException(503,'Google pricing could not be verified. Retry pricing.') from None
102
+
103
+
104
+ class MemoryPage(Target):
105
+ scope:Literal['user','project','knowledge']='project'
106
+ status:Literal['active','superseded','quarantined','all']='active'
107
+ query:str=Field(default='',max_length=200)
108
+ cursor:str|None=Field(default=None,max_length=500)
109
+ limit:int=Field(default=50,ge=1,le=100,strict=True)
110
+
111
+ class ProfileName(Strict):
112
+ name:str=Field(min_length=1,max_length=80)
113
+
114
+ @router.post('/api/memory/records')
115
+ async def memory_records(payload:MemoryPage):
116
+ return await invoke(service().browse,payload.projectId,payload.scope,status=payload.status,query=payload.query,cursor=payload.cursor,limit=payload.limit)
117
+
118
+ @router.post('/api/memory/record')
119
+ async def memory_record(payload:Forget):
120
+ return await invoke(service().record_detail,payload.projectId,payload.scope,payload.recordId)
121
+
122
+ @router.post('/api/memory/user-profile')
123
+ async def user_profile(payload:ProfileName):
124
+ return await invoke(service().rename_profile,payload.name)
125
+
126
+
127
+ class ContextRequest(Target):
128
+ query:str=Field(min_length=1,max_length=4000)
129
+
130
+ @router.post('/api/memory/context')
131
+ async def memory_context(payload:ContextRequest):
132
+ memory=service()
133
+ records,status=await invoke(memory.recall,payload.projectId,payload.query)
134
+ return {'records':records,'status':status,'userId':memory.owner,'projectId':payload.projectId,'boundary':'retrieval_only'}
135
+
136
+
137
+ @router.post('/api/memory/overview')
138
+ async def memory_overview(payload:Target):
139
+ memory=service();result=await invoke(memory.overview,payload.projectId)
140
+ result['skills']=skills.public(skills.catalogue(memory,payload.projectId))
141
+ return result
@@ -49,6 +49,7 @@ class MemoryService:
49
49
  with self.db() as db:
50
50
  db.executescript('''
51
51
  CREATE TABLE IF NOT EXISTS identity (id TEXT PRIMARY KEY);
52
+ CREATE TABLE IF NOT EXISTS profiles (id TEXT PRIMARY KEY,name TEXT NOT NULL);
52
53
  CREATE TABLE IF NOT EXISTS settings (project TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1);
53
54
  CREATE TABLE IF NOT EXISTS snapshots (project TEXT PRIMARY KEY, revision INTEGER, entities TEXT, digest TEXT);
54
55
  CREATE TABLE IF NOT EXISTS queue (seq INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT UNIQUE, project TEXT, scope TEXT, key TEXT, text TEXT, source TEXT, state TEXT, created TEXT);
@@ -61,6 +62,7 @@ class MemoryService:
61
62
  self.owner = row[0] if row else str(uuid.uuid4())
62
63
  if not row:
63
64
  db.execute('INSERT INTO identity VALUES (?)', (self.owner,))
65
+ db.execute("INSERT OR IGNORE INTO profiles VALUES (?,?)",(self.owner,'Default profile'))
64
66
 
65
67
  @contextmanager
66
68
  def db(self):
@@ -94,6 +96,7 @@ class MemoryService:
94
96
  return {'enabled': enabled}
95
97
 
96
98
  def call(self, operations):
99
+ operations=[{**op,'owner':self.owner,'actor':'user:'+self.owner if (op.get('subject')==self.subject('user','') or op.get('action')=='forget') else 'atmem-project-sync','actorType':'user' if (op.get('subject')==self.subject('user','') or op.get('action')=='forget') else 'project-sync'}for op in operations]
97
100
  result = subprocess.run([self.worker, '-I', str(ROOT / 'server/memory_worker.py')],
98
101
  input=json.dumps({'database': str(self.home / 'memories.db'), 'operations': operations}),
99
102
  text=True, capture_output=True, timeout=25, cwd=str(self.home))
@@ -202,22 +205,49 @@ class MemoryService:
202
205
  self.flush()
203
206
  return self.status(project)
204
207
 
208
+ def overview(self,project):
209
+ from .memory_context import describe
210
+ self.project(project)
211
+ with self.lock,self.db()as db:
212
+ activity=[{'id':row[0],'createdAt':row[1],**json.loads(row[2])}for row in db.execute('SELECT id,created,payload FROM activity WHERE project=? ORDER BY created DESC LIMIT 20',(project,))]
213
+ return {**self.status(project),'activity':activity,'userProfile':self.user_profile(),'contextProviders':describe()}
214
+
205
215
  def inspect(self, project):
216
+ from .memory_context import describe
206
217
  self.project(project)
207
218
  with self.lock:
208
219
  if self.enabled(project):
209
220
  self.flush()
210
221
  subjects = [self.subject('user', project), self.subject('project', project), self.subject('knowledge', project)]
211
222
  try:
212
- results = self.call([{'action': 'list', 'subject': s} for s in subjects])
213
- records = [[{k: r.get(k) for k in ('id','content','status','fact_key','created_at','supersedes_id')} for r in rows[-200:] if r.get('status') != 'tombstoned'] for rows in results]
223
+ results = self.call([{'action': 'page', 'subject': s,'status':'all','limit':50} for s in subjects])
224
+ records = [page['records'] for page in results]
214
225
  except (OSError, RuntimeError, subprocess.TimeoutExpired):
215
226
  self.error = 'AtMem unavailable — using this project only; eligible changes queued'
216
227
  records = [[], [], []]
217
228
  with self.db() as db:
218
229
  activity = [{'id': r[0], 'createdAt': r[1], **json.loads(r[2])} for r in db.execute('SELECT id,created,payload FROM activity WHERE project=? ORDER BY created DESC LIMIT 20', (project,))]
219
230
  events = [{'key':r[0],'status':r[1],'reason':r[2],'createdAt':r[3]} for r in db.execute("SELECT key,state,reason,created FROM queue WHERE project=? AND state IN ('pending','rejected') ORDER BY seq DESC LIMIT 20",(project,))]
220
- return {**self.status(project), 'profile': records[0], 'project': records[1], 'knowledge': records[2], 'activity': activity, 'events':events}
231
+ return {**self.status(project), 'profile': records[0], 'project': records[1], 'knowledge': records[2], 'activity': activity, 'events':events,'userProfile':self.user_profile(),'contextProviders':describe()}
232
+
233
+ def user_profile(self):
234
+ with self.db()as db:name=db.execute('SELECT name FROM profiles WHERE id=?',(self.owner,)).fetchone()[0]
235
+ return {'id':self.owner,'name':name,'kind':'local','identitySource':'local-installation'}
236
+
237
+ def rename_profile(self,name):
238
+ if not isinstance(name,str)or not name.strip()or len(name)>80:raise ValueError('Enter a profile name up to 80 characters')
239
+ with self.lock,self.db()as db:db.execute('UPDATE profiles SET name=? WHERE id=?',(name.strip(),self.owner))
240
+ return self.user_profile()
241
+
242
+ def browse(self,project,scope='project',**options):
243
+ self.project(project)
244
+ if scope not in {'user','project','knowledge'}:raise ValueError('Invalid memory scope')
245
+ with self.lock:return self.call([{'action':'page','subject':self.subject(scope,project),**options}])[0]
246
+
247
+ def record_detail(self,project,scope,record_id,preview=False):
248
+ self.project(project)
249
+ if scope not in {'user','project'}:raise ValueError('Invalid memory scope')
250
+ with self.lock:return self.call([{'action':'forget-preview'if preview else 'record','subject':self.subject(scope,project),'recordId':record_id}])[0]
221
251
 
222
252
  def forget(self, project, scope, record_id):
223
253
  if scope not in {'user', 'project'}:
@@ -243,12 +273,15 @@ class MemoryService:
243
273
  try:
244
274
  rows = self.call([{'action': 'recall', 'subject': self.subject(s, project), 'query': query, 'limit': 6} for s in ('user', 'project')])
245
275
  context = [{'scope': scope, 'id': r['id'], 'text': r['content'], 'sourceDate': r['created_at'], 'key': r.get('fact_key')} for scope, records in zip(('user','project'), rows) for r in records]
246
- return context, self.status(project)
276
+ from .memory_context import retrieve
277
+ external,provider_status=retrieve(project,self.owner,query,safe)
278
+ return context+external,{**self.status(project),'contextProviders':provider_status}
247
279
  except (OSError, RuntimeError, subprocess.TimeoutExpired):
248
280
  self.error = 'AtMem unavailable — using this project only; eligible changes queued'
249
281
  return [], self.status(project)
250
282
 
251
283
  def record_turn(self, project, payload):
284
+ payload={**payload,'userId':self.owner,'actor':'atmem-architect','actorType':'agent'}
252
285
  ident = str(uuid.uuid4())
253
286
  with self.lock, self.db() as db:
254
287
  db.execute('INSERT INTO activity VALUES (?,?,?,?)', (ident, project, timestamp(), json.dumps(payload)))
@@ -2,10 +2,65 @@
2
2
  import json
3
3
  import sys
4
4
  import sqlite3
5
+ import base64
6
+ import re
5
7
  from importlib.metadata import version
6
8
  from atmem import Memory
7
9
 
8
10
 
11
+ def public_record(record, owner=None, memory=None):
12
+ episode=memory.store.get_episode(record['subject_id'],record['episode_id'])if memory and record.get('episode_id')else None
13
+ raw=(episode or {}).get('raw') or record.get('raw') or {}
14
+ return {**{key:record.get(key)for key in ('id','content','status','fact_key','created_at','updated_at','supersedes_id','source_type')},
15
+ 'governance':{'userId':raw.get('user_id'), 'actor':raw.get('actor'), 'actorType':raw.get('actor_type'),
16
+ 'source':raw.get('source_url') or raw.get('interpreter'), 'ownerUserId':owner}}
17
+
18
+
19
+ def page_records(memory,subject,op):
20
+ # Pinned SDK adapter: use its policy-aware connection, never reopen the database.
21
+ from atmem.store.sqlite import _record_from_row
22
+ db=memory.store._conn
23
+ limit=max(1,min(int(op.get('limit',50)),100));query=str(op.get('query','')).strip()[:200]
24
+ status=op.get('status','active')
25
+ statuses=('active','superseded','quarantined') if status=='all' else (status,)
26
+ if any(value not in {'active','superseded','quarantined'}for value in statuses):raise ValueError('Invalid memory status')
27
+ before=None
28
+ if op.get('cursor'):
29
+ before=json.loads(base64.urlsafe_b64decode(op['cursor']).decode())
30
+ if not isinstance(before,list)or len(before)!=2 or any(not isinstance(v,str)or len(v)>100 for v in before):raise ValueError('Invalid page cursor')
31
+ candidates=[];total=0
32
+ for selected in statuses:
33
+ clauses=['subject_id=?','status=?'];args=[subject,selected]
34
+ if query:
35
+ if query.startswith('rec_'):
36
+ clauses.append('id=?');args.append(query)
37
+ else:
38
+ terms=re.findall(r'[^\W_]+',query,flags=re.UNICODE)[:12]
39
+ if not terms:raise ValueError('Enter a search word or record ID')
40
+ if not memory.store._fts_enabled:raise ValueError('Indexed search is unavailable for this store')
41
+ expression=' AND '.join('"'+word.replace('"','')+'"'for word in terms)
42
+ clauses.append('id IN (SELECT record_id FROM records_fts WHERE records_fts MATCH ? AND subject_id=?)')
43
+ args.extend([expression,subject])
44
+ where=' AND '.join(clauses)
45
+ total+=db.execute('SELECT COUNT(*) FROM records WHERE '+where,args).fetchone()[0]
46
+ if before:where+=' AND (created_at,id)<(?,?)';args.extend(before)
47
+ rows=db.execute('SELECT * FROM records WHERE '+where+' ORDER BY created_at DESC,id DESC LIMIT ?',(*args,limit+1)).fetchall()
48
+ candidates.extend(_record_from_row(row)for row in rows)
49
+ candidates.sort(key=lambda row:(row['created_at'],row['id']),reverse=True)
50
+ more=len(candidates)>limit;rows=candidates[:limit]
51
+ cursor=base64.urlsafe_b64encode(json.dumps([rows[-1]['created_at'],rows[-1]['id']]).encode()).decode()if more and rows else None
52
+ return {'records':[public_record(row,op.get('owner'),memory)for row in rows],'total':total,'hasMore':more,'nextCursor':cursor,'limit':limit}
53
+
54
+
55
+ def keyed_records(memory,subject,record_id,limit=10001):
56
+ record=memory.store.get_record(subject,record_id)
57
+ if not record or record['status']=='tombstoned':raise ValueError('Memory is unavailable in this scope')
58
+ if not record.get('fact_key'):return record,[record]
59
+ from atmem.store.sqlite import _record_from_row
60
+ rows=memory.store._conn.execute("SELECT * FROM records WHERE subject_id=? AND fact_key IS ? AND status IN ('active','superseded','quarantined') ORDER BY created_at DESC,id DESC LIMIT ?",(subject,record.get('fact_key'),limit)).fetchall()
61
+ return record,[_record_from_row(row)for row in rows]
62
+
63
+
9
64
  def run(payload):
10
65
  if version('atmem') != '2.3.10':
11
66
  raise RuntimeError('AtMem AI Architect requires the tested AtMem 2.3.10 runtime')
@@ -23,32 +78,40 @@ def run(payload):
23
78
  results.append(json.loads(previous[0]))
24
79
  continue
25
80
  value = memory.remember(subject, message=op['text'], source_type='user_message',
26
- interpreted_fact=op['text'], interpreted_fact_key=op['key'], actor='atforma',
27
- raw={'interpreter': 'atforma-project-event-v1', 'event_id': op['eventId'], 'source_url': op.get('source', '')})
81
+ interpreted_fact=op['text'], interpreted_fact_key=op['key'], actor=op.get('actor','atforma'),
82
+ raw={'interpreter': 'atforma-project-event-v1', 'event_id': op['eventId'], 'source_url': op.get('source', ''), 'user_id':op.get('owner'), 'actor':op.get('actor'), 'actor_type':op.get('actorType')})
28
83
  receipt = {'refused': value.get('refused', []), 'ids': [r['id'] for r in value.get('records', [])] + value.get('duplicate_ids', [])}
29
84
  receipts.execute('INSERT INTO receipts VALUES (?,?)', (op['eventId'], json.dumps(receipt)))
30
85
  receipts.commit()
31
86
  results.append(receipt)
32
87
  elif action == 'recall':
33
88
  results.append(memory.recall(subject, op['query'], limit=op.get('limit', 6)))
89
+ elif action == 'page':
90
+ results.append(page_records(memory,subject,op))
91
+ elif action == 'record':
92
+ record=memory.store.get_record(subject,op['recordId'])
93
+ if not record or record['status']=='tombstoned':raise ValueError('Record unavailable in this scope')
94
+ audit=memory.store._conn.execute('SELECT actor,event_type,created_at FROM audit_log WHERE subject_id=? AND record_id=? ORDER BY created_at DESC,sequence DESC LIMIT 50',(subject,op['recordId'])).fetchall()
95
+ results.append({'record':public_record(record,op.get('owner'),memory),'audit':[dict(row)for row in audit]})
96
+ elif action == 'forget-preview':
97
+ record,rows=keyed_records(memory,subject,op['recordId'],101)
98
+ total=memory.store._conn.execute("SELECT COUNT(*) FROM records WHERE subject_id=? AND fact_key IS ? AND status!='tombstoned'",(subject,record.get('fact_key'))).fetchone()[0]if record.get('fact_key')else 1
99
+ results.append({'records':[public_record(row,op.get('owner'),memory)for row in rows[:100]],'total':total})
34
100
  elif action == 'list':
35
101
  results.append(memory.list(subject, include_inactive=True))
36
102
  elif action == 'forget':
37
103
  # Forget the entire keyed preference history so old values cannot resurface.
38
- records = memory.list(subject, include_inactive=True)
39
- record = next((r for r in records if r['id'] == op['recordId'] and r['status'] != 'tombstoned'), None)
40
- if record is None:
41
- raise ValueError('Memory record is not available in this scope')
104
+ record,records=keyed_records(memory,subject,op['recordId'])
105
+ if len(records)>10000:raise ValueError('History exceeds the synchronous removal limit; no records were removed')
42
106
  deletion_receipts = []
43
107
  for r in records:
44
- if r.get('fact_key') == record.get('fact_key') and r['status'] != 'tombstoned':
45
- deletion_receipts.append(memory.forget_record(subject, r['id'])['receipt'])
108
+ deletion_receipts.append(memory.forget_record(subject,r['id'],actor=op.get('actor','user'))['receipt'])
46
109
  results.append({'deleted': True, 'receipts': deletion_receipts, 'key': record.get('fact_key')})
47
110
  elif action == 'audit':
48
111
  report = memory.audit(subject)
49
112
  results.append({'valid': report['audit_chain_valid'], 'events': report['audit_log'][-40:]})
50
113
  elif action == 'log':
51
- results.append(memory.log_action(subject, op['eventType'], op['payload'], actor='atforma'))
114
+ results.append(memory.log_action(subject, op['eventType'], op['payload'], actor='atmem-architect:'+op.get('owner','unknown')))
52
115
  else:
53
116
  raise ValueError('Unknown memory operation')
54
117
  return {'version': version('atmem'), 'results': results}
@@ -0,0 +1,90 @@
1
+ """Explicit local editor handoff. Never executes project files or overwrites a workspace."""
2
+ import base64
3
+ import binascii
4
+ import io
5
+ import os
6
+ from pathlib import Path, PurePosixPath
7
+ import re
8
+ import shutil
9
+ import stat
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import zipfile
14
+
15
+ MAX_ZIP = 32_000_000
16
+ MAX_EXPANDED = 150_000_000
17
+
18
+
19
+ def orca_command():
20
+ names = ['orca-ide', 'orca'] if sys.platform.startswith('linux') else ['orca']
21
+ for name in names:
22
+ found = shutil.which(name)
23
+ if found:
24
+ return found
25
+ candidate = Path.home()/'.local/bin/orca'
26
+ if candidate.is_file() and os.access(candidate, os.X_OK):
27
+ return str(candidate)
28
+ raise ValueError('Install Orca and enable its CLI in Settings → General → Orca CLI.')
29
+
30
+
31
+ def unpack_package(encoded, root, project):
32
+ try:
33
+ data = base64.b64decode(encoded, validate=True)
34
+ except (ValueError, binascii.Error):
35
+ raise ValueError('The project ZIP is invalid.') from None
36
+ if not data or len(data) > MAX_ZIP:
37
+ raise ValueError('Orca handoff supports ZIPs up to 32 MB. Download ZIP instead.')
38
+ try:
39
+ archive = zipfile.ZipFile(io.BytesIO(data))
40
+ entries = archive.infolist()
41
+ if len(entries) > 20000 or sum(e.file_size for e in entries) > MAX_EXPANDED:
42
+ raise ValueError('The expanded project exceeds the handoff limit.')
43
+ seen = set()
44
+ for entry in entries:
45
+ name = entry.filename
46
+ parts = PurePosixPath(name).parts
47
+ mode = entry.external_attr >> 16
48
+ if (not parts or name.startswith('/') or '\\' in name or '\x00' in name
49
+ or any(part in {'.', '..'} or ':' in part or part.endswith((' ', '.')) for part in parts)
50
+ or any(re.match(r'^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)', part, re.I) for part in parts)
51
+ or mode and stat.S_IFMT(mode) not in {0, stat.S_IFREG, stat.S_IFDIR}):
52
+ raise ValueError('The ZIP contains an unsupported path or file type.')
53
+ canonical = name.rstrip('/').casefold()
54
+ if canonical in seen:
55
+ raise ValueError('The ZIP contains conflicting paths.')
56
+ seen.add(canonical)
57
+ if not entries:
58
+ raise ValueError('The project ZIP is empty.')
59
+ root = Path(root)
60
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
61
+ destination = Path(tempfile.mkdtemp(prefix=project+'-', dir=root))
62
+ try:
63
+ for entry in entries:
64
+ path = destination.joinpath(*PurePosixPath(entry.filename).parts)
65
+ if entry.is_dir():
66
+ path.mkdir(parents=True, exist_ok=True)
67
+ continue
68
+ path.parent.mkdir(parents=True, exist_ok=True)
69
+ with archive.open(entry) as source, path.open('xb') as target:
70
+ shutil.copyfileobj(source, target)
71
+ os.chmod(path, 0o700 if (entry.external_attr >> 16) & 0o111 else 0o600)
72
+ except Exception:
73
+ shutil.rmtree(destination)
74
+ raise
75
+ return destination
76
+ except (zipfile.BadZipFile, RuntimeError, OSError):
77
+ raise ValueError('The project ZIP could not be prepared for Orca.') from None
78
+
79
+
80
+ def open_package(encoded, root, project):
81
+ command = orca_command()
82
+ destination = unpack_package(encoded, root, project)
83
+ try:
84
+ subprocess.run([command, 'open', '--json'], check=True, capture_output=True, timeout=30)
85
+ subprocess.run([command, 'repo', 'add', '--path', str(destination), '--kind', 'folder', '--json'],
86
+ check=True, capture_output=True, timeout=30)
87
+ except (OSError, subprocess.SubprocessError):
88
+ # Keep the prepared folder for retry/manual opening. Do not expose command output.
89
+ return {'opened': False, 'path': str(destination), 'message': 'Orca did not open. Open this folder in Orca.'}
90
+ return {'opened': True, 'path': str(destination)}
package/server/pricing.py CHANGED
@@ -82,3 +82,109 @@ def storage_quote(provider,region):
82
82
  if len(candidates)==1:return candidates[0]
83
83
  return {'available':False,'provider':'gcp','region':region,'sourceUrl':PROVIDER_PAGES[provider],'reason':'An exact regional storage SKU needs a configured Google Cloud Billing Catalog API key or a sourced quote. A generic/default-region web price will not be substituted.','nextQuestion':'Which Google Cloud region and storage class should we price?'}
84
84
  return {'available':False,'provider':'ibm','region':region,'sourceUrl':PROVIDER_PAGES[provider],'reason':'IBM pricing depends on region, storage class and plan. No verified regional quote is configured; no AWS or invented rate is substituted.','nextQuestion':'Which IBM region and storage class or existing IBM quote should we use?'}
85
+
86
+
87
+ def parse_lambda(data,region,url,architecture='x86_64'):
88
+ """Regional on-demand base meters; never apply account-specific free tiers."""
89
+ suffix='-ARM' if architecture=='arm64' else ''
90
+ expected={'requests':('AWS-Lambda-Requests'+suffix,'Request'),
91
+ 'compute':('AWS-Lambda-Duration'+suffix,'Lambda-GB-Second')}
92
+ meters={}
93
+ for name,(group,unit)in expected.items():
94
+ matches=[]
95
+ for sku,product in data.get('products',{}).items():
96
+ attrs=product.get('attributes',{})
97
+ if attrs.get('regionCode')!=region or attrs.get('group')!=group:continue
98
+ for term in data.get('terms',{}).get('OnDemand',{}).get(sku,{}).values():
99
+ for rate in term.get('priceDimensions',{}).values():
100
+ if rate.get('unit')!=unit or rate.get('beginRange')!='0':continue
101
+ value=float(rate.get('pricePerUnit',{}).get('USD','nan'))
102
+ if not 0<value<1:continue
103
+ end=rate.get('endRange')
104
+ matches.append({'price':value,'unit':unit,'service':'AWS Lambda / '+sku,
105
+ 'tierLimit':None if end=='Inf'else float(end)})
106
+ if len(matches)!=1:raise ValueError('Could not uniquely verify the regional Lambda meter')
107
+ meters[name]=matches[0]
108
+ return {'available':True,'provider':'aws','region':region,'architecture':architecture,
109
+ 'currency':'USD','source':'AWS public Price List','url':url,
110
+ 'date':datetime.now(timezone.utc).date().isoformat(),'publicationDate':data.get('publicationDate'),
111
+ 'meters':meters,'coverage':'On-demand requests and base compute only. Free tiers, logs, transfer, public gateway charges, provisioned concurrency, taxes and discounts are not included.'}
112
+
113
+
114
+ @lru_cache(maxsize=32)
115
+ def _lambda(region,architecture,hour):
116
+ url=f'https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AWSLambda/current/{region}/index.json'
117
+ with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'AtMem AI Architect/0.2'}),timeout=12)as response:raw=response.read(12_000_001)
118
+ if len(raw)>12_000_000:raise ValueError('Price list exceeds the supported size')
119
+ return parse_lambda(json.loads(raw),region,url,architecture)
120
+
121
+
122
+ def lambda_quote(region,architecture='x86_64'):
123
+ if not re.fullmatch(r'[a-z]{2}-(?:gov-)?[a-z]+-\d',region)or architecture not in {'x86_64','arm64'}:
124
+ raise ValueError('Select an explicit AWS region and Lambda architecture')
125
+ return dict(_lambda(region,architecture,int(time.time()//3600)))
126
+
127
+
128
+ def _public_rows(html):
129
+ from html.parser import HTMLParser
130
+ class Rows(HTMLParser):
131
+ def __init__(self):
132
+ super().__init__();self.rows=[];self.row=None;self.cell=None
133
+ def handle_starttag(self,tag,attrs):
134
+ if tag=='tr':self.row=[]
135
+ if tag in {'td','th'} and self.row is not None:self.cell=[]
136
+ def handle_data(self,data):
137
+ if self.cell is not None:self.cell.append(data)
138
+ def handle_endtag(self,tag):
139
+ if tag in {'td','th'} and self.cell is not None:
140
+ self.row.append(' '.join(' '.join(self.cell).split()));self.cell=None
141
+ if tag=='tr' and self.row is not None:self.rows.append(self.row);self.row=None
142
+ parser=Rows();parser.feed(html);return parser.rows
143
+
144
+
145
+ def parse_google_public(html,service,url):
146
+ import hashlib
147
+ if service=='cloud-run':
148
+ marker='Services (Requests-based billing)'
149
+ if marker not in html:raise ValueError('Cloud Run request-based pricing section is missing')
150
+ html=html[html.index(marker):]
151
+ expected={'cpu':'CPU (per vCPU-second)','memory':'Memory (per GiB-second)','requests':'Requests (per 1,000,000)'}
152
+ units={'cpu':'vCPU-seconds','memory':'GiB-seconds','requests':'million requests'}
153
+ elif service=='cloud-sql':
154
+ if 'Iowa (us-central1)' not in html or 'Enterprise Edition - N4' not in html:
155
+ raise ValueError('Cloud SQL reference region or edition is missing')
156
+ marker='Enterprise Edition - N4';html=html[html.index(marker):]
157
+ expected={'cpu':'vCPUs','memory':'Memory'};units={'cpu':'vCPU-hours','memory':'GiB-hours'}
158
+ else:raise ValueError('Unsupported Google pricing service')
159
+ meters={}
160
+ for row in _public_rows(html):
161
+ if not row:continue
162
+ for key,label in expected.items():
163
+ if key in meters or row[0]!=label:continue
164
+ value=next((re.search(r'\$([0-9]+(?:\.[0-9]+)?)',cell)for cell in row[1:]if '$' in cell),None)
165
+ if value:
166
+ price=float(value.group(1))
167
+ if not 0<price<10:raise ValueError('Unexpected Google public price')
168
+ meters[key]={'price':price,'unit':units[key]}
169
+ if len(meters)==len(expected):break
170
+ if len(meters)!=len(expected):raise ValueError('Google public pricing meters could not be verified')
171
+ return {'available':True,'provider':'gcp','service':service,'region':'us-central1','currency':'USD',
172
+ 'source':'Google Cloud public pricing','url':url,'date':datetime.now(timezone.utc).date().isoformat(),
173
+ 'sourceSha256':hashlib.sha256(html.encode()).hexdigest(),'meters':meters,
174
+ 'coverage':'US reference on-demand rates. Excludes free tiers, commitments, taxes, networking, backups, storage, external APIs and payment fees.'}
175
+
176
+
177
+ @lru_cache(maxsize=8)
178
+ def _google_public(service,hour):
179
+ path={'cloud-run':'run','cloud-sql':'sql'}.get(service)
180
+ if not path:raise ValueError('Unsupported Google pricing service')
181
+ url=f'https://cloud.google.com/{path}/pricing'
182
+ with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'AtMem Architect pricing'}),timeout=15)as response:raw=response.read(8_000_001)
183
+ if len(raw)>8_000_000:raise ValueError('Google price page exceeds its size bound')
184
+ return parse_google_public(raw.decode('utf-8'),service,url)
185
+
186
+
187
+ def google_public_quote(service,region,planning_reference=False):
188
+ if region!='us-central1' and not (planning_reference and (not region or re.search('select|unknown|tbd',region,re.I))):
189
+ raise ValueError('This public quote supports Iowa only; the selected region is preserved.')
190
+ return {**_google_public(service,int(time.time()//3600)),'planningReference':region!='us-central1'}
package/server/prompts.py CHANGED
@@ -121,3 +121,16 @@ new project; preserve an existing client's chosen name. The host constructs a co
121
121
  proposal after validation. Do not spend output tokens copying the entire current graph,
122
122
  original brief or unchanged configuration just to propose a small correction.
123
123
  """
124
+
125
+
126
+ SYSTEM_PROMPT += """
127
+ # Owner UX instruction: avoid word vomit
128
+ Keep the visible chat reply quick and concise: normally one or two short sentences,
129
+ at most 20 words total. Longer optional explanation belongs behind a help disclosure or in the specification. Ask one direct question when a decision is needed.
130
+ Do not recap the brief, repeat accepted choices, narrate your process, advertise a
131
+ skill or add examples unless asked. Example: 'Use USD for welcome credit and top-ups?'
132
+ Preserve full requirements, rationale, uncertainty and technical detail in structured
133
+ project/specification fields. Do not shorten or discard source requirements.
134
+ If a material blocker needs explaining, state its cause and next action briefly.
135
+ User requests for detailed specifications still receive complete specifications.
136
+ """